/*
 * Copyright (c) 2007 Sun Microsystems, Inc. All rights reserved
 * Use is subject to license terms.
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* TODO:
 *  - test out the PUT farm in atom/x/
 */

/* mod_atom.c: Minimal implementation of the Atom Publishing Protocol, which
 *  also involves generating Atom feeds.
 *
 * - Tim Bray (started 2007/04/26)
 */

/* For this first cut, we're all convention over configuration, so there's
 *  just one one directive, AtomPub:
 *
 * AtomPub /public/uri/path /real/directory/where/data/lives "Title" "Author"
 *
 * Let's call the root, either of URI space or directory space, D.  There is
 *  is a big directory tree underneath it which is mapped 1-for-1 between
 *  URI space and filesystem space.  However, there are two copies of
 *  the URI space rooted at D/atom/ and D/pub/.  The convention is that
 *  D/atom/ is where do AtomPub CRUD operations, and D/pub/ is the published
 *  URI space that the world fetches from.  mod_atom enforces that PUT, POST,
 *  and DELETE operations are only run through the D/atom/-rooted part of
 *  of URI space, and that atom entries can only be fetched through there.
 * The idea is that you can use <Location > based
 *  protection to require authentication for non-GET operations.
 *
 * In fact, the directory space is rooted at D/pub/; D/atom/ doesn't exist; see
 *   map_to_storage().
 *
 * So in the discussion which follows, things are referred to as either
 *  D/atom/ or D/pub/ reflecting how you'd use them.  But you *could* fetch
 *  the service document D/atom/pub.atomsvc by doing a GET on
 *  D/pub/pub.atomsvc
 *
 *  D/pub/service.atomsvc the service document.
 *
 * The front page for a publication, constructed from D/e/entries is
 *  D/index.html
 * The default atom feed, constructed from D/e/entries, is
 *  D/feed.atom
 *
 * The directory structure distinguishes between entry and media collections;
 *  to start with, we'll just have two named "entries" and "media".
 *
 * D/pub/e/ - entry collections live here.
 * D/pub/m/ - media collections live here.
 *
 * For each entry collection Fe, and entry E:
 *
 * D/pub/e/Fe/feed - its feed.
 * D/atom/e/Fe/collection - its Collection for AtomPub operations.
 * D/pub/e/Fe/YYYY/MM/DD/E.html - Per-entry HTML pages
 * D/atom/e/Fe/YYYY/MM/DD/E.atom - Per-entry Atom entry
 * - Note that the .html version appears only for Atom entries which
 *   have "html", "xhtml", or "text" in <content type=
 *
 * For each media collection Fm and media resource M:
 *
 * D/pub/m/Fm/YYYY/MM/DD/M.atom - the MLE
 * D/pub/m/Fm/YYYY/MM/DD/M - the bits
 * D/pub/m/Fm/YYYY/MM/DD/M.mt - file containing X's media-type
 *
 * Extras:
 * D/pub/x - the "Extras" subtree; clients can PUT anything they want
 *            in here; some things are pre-supplied:
 *  D/pub/x/templates/entry - HTML template for an entry
 *  D/pub/x/templates/front - HTML template for a collection's front page
 *  D/pub/x/css/entry.css - CSS for individual entries in D/pub/
 *  D/pub/x/css/front.css - CSS for front page in D/pub/
 *  D/pub/x/js/entry.js - JavaScript for individual entries in D/pub/
 *  D/pub/x/js/front.js - JavaScript for front page in D/pub/
 *  D/pub/x/options - Files can be placed here to control options
 *  D/pub/x/options/html - Signifies that HTML should be generated when
 *                          atom entries are POSTED or PUT or DELETED.
 *
 * Meta-publications:
 * Suppose the publication is a metapub, described with an
 *  AtomMetaPub directive.  New publications may be created and deleted, and
 *  their metadata updated, by POST/DELETE/PUT to its collection URI.
 *
 * The metapub's URI and directory space will be rooted at the requested
 *  paths with an additional level of subdirectory.
 * There will be a subdirectories "s00" .. "s99", where the sub-publications
 *  are rooted.  A sub-publication's root directory is computed from the 
 *  title (or Slug, if provided) of the new entry at POST time, and the 
 *  s00 .. s99 is chosen at random.
 * So if the slug/title gave a name of "toenails", the pub might be rooted
 *  at D/s32/toenails/, its service doc at D/s32/toenails/atom/pub.atomsvc
 * Each entry in a metapub's collection describes
 *  a publication, including the author and title.  The publication's base URI
 *  and base directory can be computed from the <link rel="publication" />
 *  value.
 *
 * Lots of pieces of code below are extremely hard-wired to this layout,
 *  and changing it will require a lot of re-thinking.
 */

/*
 * Module workflow (*h means routine name is same as hook name)
 * 1. init phase
 *  "pre_config" (*h), alloc the pubs array
 *  "atompub_directive" sticks the basics about a pub in the pubs array
 *  "post_config" (*h) validates the pubs that were declared and builds
 *  the pubs index.
 * 2. runtime.  Everything first checks the find_pub routine, which looks
 *  up a URI and returns a pub_t pointer or NULL. We have three runtime
 *  hooks in:
 *  "map_to_storage" (*h) checks to see if the request->uri belongs to
 *    one of our pubs.  If so, further checks to see if it can simply be
 *    served statically as a file, in which case it fills in request->filename.
 *  "type_checker" (*h) figures out our type; since we don't use file
 *    extensions, we have to fill it in manually.
 *  "handler" (*h) takes care of the actual heavy lifting in the POST and
 *    PUT methods.
 */

#include "mod_atom.h"
#include "children.h"
#include "html_templates.h"

/* A bunch of regexes for sorting incoming URIs into buckets
 */
typedef struct pub_patterns_t {
    ap_regex_t * entry;
    ap_regex_t * collection;
    ap_regex_t * media_object;
    ap_regex_t * entries_space;
    ap_regex_t * extras_space;
    ap_regex_t * media_space;
} pub_patterns_t;

/* A publication can be regular, or a meta-pub, or a sub-pub created
 *  by posting to a meta-pub
 */
typedef enum pub_type_t {
    REGULAR_PUB, META_PUB, SUB_PUB
} pub_type_t;

/* a publication, corresponding to an AtomPub directive in config */
typedef struct pub_t {
    char * path; /* the path used by the world to refer to this pub */
    char * dir; /* the directory where the pub's data is rooted */
    char * title; /* the pub's human-readable title */
    char * author; /* default author/name */

    char * options_html_file;
    pub_patterns_t * patterns;
    pub_type_t type;

} pub_t;

/* fake low-rent continuation in run_fp_template */
typedef enum template_state_t {
    Writing,
    Bypassing,
    Done
} template_state_t;

/* Package of information about an Atom entry.  It gets written and read in
 *  a fairly ad-hoc fashion by lots of routines.  This is probably not
 *  strictly necessary and arguably represents unnecessary data coupling.
 *  However, it does have the benefit of reducing almost all calls to just
 *  a couple of arguments, making things easier to read.
 */
typedef struct entry_t {
    apr_xml_doc * xml; /* DOM of the entry */
    char * path;       /* the part after ...(atom|pub) & with no extension */
    pub_t * pub;       /* the publication */
    char * coll_base;  /* dir for this entry's collection & feed */
    char * id;         /* unique id */
    int size;          /* in bytes, for media objects only */
    text_construct_type_t type_of_content;
} entry_t;

/* "forward" he cried from the rear
 */
static char * atom_dir(char * name, apr_pool_t * p);
static char * atom_time_at(apr_time_t when, apr_pool_t * pool);
static int build_index(apr_pool_t * pperm, apr_pool_t * ptemp);
static char * build_pub(pub_t * pub, apr_pool_t * pperm, apr_pool_t * ptemp);
static char * create_sub_pub(request_rec * r, entry_t * e,
                             char * * sub_uri);
static char * decode_slug(char * slug, apr_pool_t * p);
static int delete(request_rec * r, pub_t * pub);
static char * dir_of(apr_pool_t * pool, char * path);
static char * entry_filename(apr_pool_t * pool, entry_t * e);
static int find_num(char * * a_p);
static char * find_coll_base(request_rec * r, pub_t * pub);
static pub_t * find_pub(request_rec * r);
static char * generate_collection(char * dirname, apr_pool_t * pool,
                                  pub_t * pub,
                                  void * writer,
                                  int start,
                                  int count);
static int get(request_rec * r, pub_t * pub);
static char * get_author(pub_t * pub, apr_pool_t * pool);
static char * get_title(pub_t * pub, apr_pool_t * pool);
static int index_step(int * state, int * mask, int prefix_length,
                      apr_pool_t * pperm, apr_pool_t * pscratch);
static void insert_attr(request_rec * r, apr_xml_elem * e,
                        char * name, char * value);
static apr_xml_elem * insert_simple_child(request_rec * r,
                                          apr_xml_elem * parent,
                                          int ns_ind, char * type,
                                          char * content);
static char * insert_text_content(request_rec * r, apr_xml_elem * e, char * t);
static int is_draft(apr_pool_t * pool, apr_xml_doc * doc);
static int is_html_enabled(apr_pool_t * pool, pub_t * pub);
static char * is_prefix(char * s, char * prefix);
static int post(request_rec * r, pub_t * pub);
static int post_entry(request_rec * r, entry_t * e);
static int post_media(request_rec * r, pub_t * pub);
static char * prepare_entry(request_rec * r, entry_t * e, char * sub_uri);
static int put(request_rec * r, pub_t * pub);
static int put_entry(request_rec * r, pub_t * pub, entry_t * e);
static int put_media(request_rec * r, pub_t * pub, entry_t * e);
static int replace_media(request_rec * r, entry_t * e);
static char * rm_rf(apr_pool_t * pool, char * dir);
static int save_new_entry(request_rec * r, entry_t * e);
static int save_new_media(request_rec *, entry_t * e);
static int server_error(request_rec * r, apr_status_t status, char * fmt, ...);
static int simple_put(request_rec * r, pub_t * pub);
static char * temp_filename(apr_pool_t * pool, char * filename);
static char * to_file(char * filename, char * data, apr_pool_t * pool);
static int update_mle(request_rec * r, entry_t * e);
static void update_public_site(request_rec * r, pub_t * pub, char * coll_base);
static char * write_collection(apr_pool_t * pool, pub_t * pub,
                               char * name, int size);
static char * write_element(apr_xml_doc * doc, apr_xml_elem * e, genxWriter gw,
                            int filter, apr_pool_t * pool);
static char * write_entry(request_rec * r, apr_xml_doc * doc, char * filename);
static void write_html_entry(request_rec * r, entry_t * e);
static int xnarf(char * filename, apr_xml_doc * * ret, apr_pool_t * pool);
static char * yyyymmdd_at(apr_time_t when, apr_pool_t * pool);

/* our globals */
/* These should be OK regardless of what sort of process/thread model we're
 *  using.  Everything is allocated at config time & thus should be
 *  inherited by subsequent code.  At request time, it's all 100% read-only,
 *  these are just lookup indices.
 */
static apr_array_header_t * pubs_from_config = NULL; /* list of known pubs */
static apr_array_header_t * pub_index = NULL; /* index for mapping uri->pub */
static genxWriter char_util; /* for char classes */
static ap_regex_t * sub_path_el;

/******** Config Time **************/

/* init our 'pubs' global index & iterator */
static int pre_config(apr_pool_t * p_conf, apr_pool_t * p_log,
                      apr_pool_t * p_temp) {
    pubs_from_config = apr_array_make(p_conf, 10, sizeof (pub_t));
    atom_iter_init(p_conf);
    return OK;
}

/* Config has been processed.  Initialize the world. */
static int post_config(apr_pool_t * pconf, apr_pool_t * plog,
                       apr_pool_t * ptemp, server_rec * s) {
    int i;
    pub_t * publist;
    char * problem;

    /* now we'll build some per-pub data structures */
    publist = (pub_t *) pubs_from_config->elts;
    for (i = 0; i < pubs_from_config->nelts; i++) {
        pub_t * pub = publist + i;

        /* make sure this pub exists */
        if ((problem = build_pub(pub, pconf, ptemp))) {
            ap_log_error(APLOG_MARK, APLOG_ERR, 0, s,
                         "Can't initialize publication '%s' at URI '%s': '%s'",
                         pub->title, pub->path, problem);
            continue;
        } else {
            ap_log_error(APLOG_MARK, APLOG_INFO, 0, s,
                         "Publication '%s' at '%s' successfully loaded!",
                         pub->title, pub->path);
        }
    }

    /* build the pub-lookup index */
    if (build_index(pconf, ptemp))
        /* something went wrong */
        return DECLINED;

    /* a genxWriter used solely to look up character classes to support
     *  URI sanitization.  Never actually used to generate XML.
     */
    char_util = genxNew(NULL, NULL, NULL);

    /* a regex to match the front of a meta sub */
    sub_path_el = ap_pregcomp(pconf,
                              apr_psprintf(pconf, "^%s[0-9][0-9]", METAPUB_SUBS),
                              AP_REG_EXTENDED);

    /* Let the filterer buld its tables */
    atom_filter_init(pconf);

    return OK;
}

/* record the existence of a pub, meta or not.
 */
static void notice_pub(cmd_parms * cmd, char * path, char * dir,
                       char * title, char * author, pub_type_t type) {
    pub_t * pub;
    pub_t * publist;
    int i;

    /* O(N**2), sigh */
    publist = (pub_t *) pubs_from_config->elts;
    for (i = 0; i < pubs_from_config->nelts - 1; i++)
        if (is_prefix(path, publist[i].path) ||
                is_prefix(publist[i].path, path)) {
            ap_log_error(APLOG_MARK, APLOG_ERR, 0, cmd->server,
                         "One publication prefixes another: '%s' and '%s'",
                         path, publist[i].path);
            return;
        }

    pub = (pub_t *) apr_array_push(pubs_from_config);
    pub->path = apr_pstrdup(cmd->pool, path);
    pub->dir = apr_pstrdup(cmd->pool, dir);
    pub->title = apr_pstrdup(cmd->pool, title);
    pub->author = apr_pstrdup(cmd->pool, author);
    pub->type = type;
}

/* deal with AtomMetaPub directive, which has to look like
 * AtomPub /base/of/uri/space /base/of/filesystem/space title author
 */
static const char * atommetapub_directive(cmd_parms * cmd, void * dummy,
                                          int argc, char * const argv[]) {

    if (argc != 4)
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, cmd->server,
                     "AtompMetaPub must have 4 arguments: URI path, directory, "
                     "title, and author");
    else
        notice_pub(cmd, argv[0], argv[1], argv[2], argv[3], META_PUB);
    return NULL;
}

/* deal with AtomPub directive, which has to look like
 * AtomPub /base/of/uri/space /base/of/filesystem/space title author
 */
static const char * atompub_directive(cmd_parms * cmd, void * dummy,
                                      int argc, char * const argv[]) {
    if (argc != 4)
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, cmd->server,
                     "AtompPub must have 4 arguments: URI path, directory, "
                     "title, and author");
    else
        notice_pub(cmd, argv[0], argv[1], argv[2], argv[3], REGULAR_PUB);

    return NULL;
}

/***************** Request Time ***********************/

/* Translates r->uri into r->filename.
 * 1. swizzle from URI space into directory space
 * 2. swizzle from D/atom/ into D/pub/
 * 3. enforces PUT/POST/DELETE through ATOM_DIR URIs
 * 4. enforces GETting atom entries and collections through  ATOM_DIR URIs
 */
static int map_to_storage(request_rec * r) {
    pub_t * pub = find_pub(r);
    apr_finfo_t * finfo;
    apr_status_t status;
    char * s;
    char * post_atom;

    if (pub == NULL)
        return DECLINED;

    /* Here's where we enforce the policy - no updates except through
     *  URIs beginning with pub->path + ATOM_DIR.  Also, you can't get
     *  Atom Entries unless you're using an ATOM_DIR uri (to allow supporting
     *  app:draft).  At the same time,  URIs in atom/ get rewritten into pub/
     */
    s = is_prefix(r->uri, pub->path) + 1;
    if ((post_atom = is_prefix(s, ATOM_DIR)))
        s = atom_join(r->pool, pub->dir, PUB_DIR, post_atom, NULL);
    else {
        /* not in atom/ subdir XXX user_error */
        if (r->method_number == M_PUT || r->method_number == M_POST ||
                r->method_number == M_DELETE) {
            apr_table_set(r->headers_out, "Allow", "GET, HEAD");
            return HTTP_METHOD_NOT_ALLOWED;
        }
        /* if it's an Atom entry */
        if (MATCHER(pub->patterns->entry, r->uri) == 0)
            return HTTP_FORBIDDEN; /* XXX user_error */

        s = atom_join(r->pool, pub->dir, s, NULL);
    }
    r->canonical_filename = r->filename = s; /* why 'canonical' too? */

    /* it seems we have to fill in r->finfo too */
    finfo = (apr_finfo_t *) apr_palloc(r->pool, sizeof (apr_finfo_t));
    finfo->name = r->filename;
    status = apr_stat(finfo, r->filename,
                      APR_FINFO_MTIME | APR_FINFO_INODE | APR_FINFO_SIZE,
                      r->pool);
    r->finfo = *finfo;

    /* It has to exist... */
    if (status == APR_SUCCESS)
        return OK;

    /* ...  unless this is a PUT into the extras space */
    if (r->method_number == M_PUT &&
            MATCHER(pub->patterns->extras_space, r->uri) == 0)
        return OK;

    return HTTP_NOT_FOUND;
}

/* Figure out the media type for our resources.  We toss some of the work back
 *  to httpd by applying .html and .xhtml extensions to some things as
 *  appropriate.
 */
static int type_checker(request_rec * r) {
    pub_t * pub;

    /* POST/PUT/DELETE, content-type depends on whether it worked */
    if (r->method_number != M_GET)
        return DECLINED;

    if ((pub = find_pub(r)) == NULL)
        return DECLINED;

    /* It's a GET in our space; 2 things to watch out for:
     *  1. Individual atom entries, for which we (pedantically) supply the
     *     trailing ";type=entry"
     *  2. Media objects, where we have to fish the media-type out of the .mt
     *     file.
     */
    /* if it's an entry */
    if (MATCHER(pub->patterns->entry, r->uri) == 0) {
        ap_set_content_type(r, "application/atom+xml;type=entry");
        return OK;
    }

    /* If it's a media resource or an edit-media thingie, append ".mt" to the
     *  end of the filename and look up the media-type, which we stashed in
     *  there.
     */
    if (MATCHER(pub->patterns->media_object, r->uri) == 0) {

        char * mt_name =
                apr_psprintf(r->pool, "%s.%s", r->filename, MEDIA_TYPE_SUFFIX);
        char * problem;
        char * media_type;

        if ((problem = atom_snarf(mt_name, &media_type, r->pool)))
            return server_error(r, APR_SUCCESS,
                                "Can't snarf media-type file '%s': %s (u %s m %s)",
                                mt_name, problem, r->uri, r->method);

        ap_set_content_type(r, media_type);
        return OK;
    }

    /* back to httpd */
    return DECLINED;
}

static int handler(request_rec * r) {
    pub_t * pub;

    /* GET should mostly be served out of a static file, let httpd do it.
     *  The exception is a collection with arguments, as in paging wanted.
     * PUT/POST/DELETE get special handling.
     * We don' need no steenking other methods.
     */
    if (r->method_number == M_GET && r->args == NULL)
        return DECLINED;

    if ((pub = find_pub(r)) == NULL)
        return DECLINED;

    switch (r->method_number) {
    case M_GET: return get(r, pub);
    case M_POST: return post(r, pub);
    case M_PUT: return put(r, pub);
    case M_DELETE: return delete(r, pub);
    default: return DECLINED;
    }
}

/* only called if there are args on the URI */
static int get(request_rec * r, pub_t * pub) {
    int start = -1;
    int count = -1;
    char * a = r->args;
    char * problem;
    char * dirname;
    void * writer;

    /* we only allow them on collection */
    if (MATCHER(pub->patterns->collection, r->uri) != 0)
        return HTTP_NOT_FOUND;

    /* the only ones we allow are start= and count=
     * Shouldn't there be a utility to sort this crap out?
     */
    while (*a) {
        if (strncmp(a, "start=", 6) == 0) {
            a += 6;
            start = find_num(&a);
            if (start == -1)
                return HTTP_NOT_FOUND;
        } else if (strncmp(a, "count=", 6) == 0) {
            a += 6;
            count = find_num(&a);
            if (count == -1)
                return HTTP_NOT_FOUND;
        } else
            return HTTP_NOT_FOUND;

        if (*a == '&')
            a++;
    }

    if (count == -1)
        count = DEFAULT_ENTRIES_IN_FEED;
    if (start == -1)
        start = 0;

    /* OK, fire the collection down the pipe */
    writer = atom_genx_client_writer(r->pool, r, &problem);
    if (!writer)
        return server_error(r, APR_SUCCESS, "Can't create XML writer: %s",
                            problem);

    dirname = dir_of(r->pool, r->filename);
    problem = generate_collection(dirname, r->pool, pub, writer, start, count);
    if (problem)
        return server_error(r, APR_SUCCESS, "Can't generate collection: %s",
                            problem);

    return OK;
}

static int post(request_rec * r, pub_t * pub) {
    char * media_type;
    entry_t entry;

    entry.pub = pub;

    /* has to be a collection */
    if (MATCHER(entry.pub->patterns->collection, r->uri) != 0) {
        /* grr... required by 2616.
         * How should I know which methods are allowed on their bogus URI
         */
        apr_table_set(r->headers_out, "Allow", "GET, HEAD");
        return HTTP_METHOD_NOT_ALLOWED;
    }

    /* OK, this is an attempt to post to a collection.
     * - Check that the content type is OK for this collection
     */
    media_type = (char *) apr_table_get(r->headers_in, "Content-Type");
    if (media_type == NULL)
        return HTTP_UNSUPPORTED_MEDIA_TYPE;
    if (MATCHER(pub->patterns->entries_space, r->uri) == 0) {
        if (strcmp(media_type, "application/atom+xml;type=entry") != 0 &&
                strcmp(media_type, "application/atom+xml") != 0)
            return HTTP_UNSUPPORTED_MEDIA_TYPE;
        return post_entry(r, &entry);
    } else if (MATCHER(pub->patterns->media_space, r->uri) == 0) {
        return post_media(r, entry.pub);
    } else
        return server_error(r, APR_SUCCESS,
                            "Weird media type '%s' on POST '%s'",
                            media_type, r->uri);

    /* This is to compute an etag on the created object at POST time,
     * so that a client doesn't have to do a post-POST GET to get it;
     * the code below has to be moved into post_entry and post_media I think
     * status = apr_stat(&finfo, r->filename,
     * APR_FINFO_MTIME|APR_FINFO_INODE|APR_FINFO_SIZE, r->pool);
     * r->mtime = r->finfo.mtime = finfo.mtime;
     * r->finfo.size = finfo.size;
     * r->finfo.inode = finfo.inode;
     * etag = ap_make_etag(r, 0);
     */

}

/* Update an entry.  We have to do a trick here to enable producing collections
 *  by walking the file tree.  When we PUT to an entry in yyyy/mm/dd, we create
 *  a file named L.<random-numbers> in today's yyyy/mm/dd which contains only
 *  the real name of the file we just updated with PUT.  The
 *  tree-walker in iterator.c knows about these things.  Of course, they can
 *  get out of date if there are subsequent PUTs or DELETEs, but that's OK,
 *  the iterator notices this and cleans up as it goes.
 */
static int put(request_rec * r, pub_t * pub) {
    apr_global_mutex_t * mutex;
    apr_status_t status;
    char * media_type;
    char * etag;
    char * if_match;
    int is_entry;
    apr_finfo_t finfo;
    entry_t entry;
    char * problem;
    int exists;
    int is_extra;

    exists = atom_file_exists(r->filename, r->pool);
    is_extra = (MATCHER(pub->patterns->extras_space, r->uri) == 0);

    /*  exists is_extra
     *  Y      Y     replace, require conditional PUT
     *  Y      N     replace, require conditional PUT, require Entry or
     *                Media resource, retain Entry ID, update app:updated
     *  N      Y     just blast it in
     *  N      N     forbidden
     */
    if (!exists) {
        if (!is_extra)
            return HTTP_FORBIDDEN; /* XXX user_error */
        else
            return simple_put(r, pub);
    }

    /* updating, gotta have that conditional put */
    /* XXXX user_error */
    if_match = (char *) apr_table_get(r->headers_in, "If-Match");
    if (if_match == NULL || if_match[0] == '*') {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Attempt to PUT to '%s' without an If-Match header",
                     r->uri);
        apr_table_set(r->headers_out, "Allow", "GET, HEAD");
        return HTTP_METHOD_NOT_ALLOWED;
    }

    if (is_extra)
        return simple_put(r, pub);

    /* updating into publication space */

    /* entry? */
    if (MATCHER(pub->patterns->entry, r->uri) == 0) {

        /* Note that it's a little dangerous to allow unconstrained PUT on
         *  a MLE, they might bust the content@src link.  Well, don't do
         *  that.
         */

        /* sanity-check media-type */
        media_type = (char *) apr_table_get(r->headers_in, "Content-Type");
        if ((media_type == NULL) ||
                (strcmp(media_type, "application/atom+xml;type=entry") != 0 &&
                    strcmp(media_type, "application/atom+xml") != 0))
            return HTTP_UNSUPPORTED_MEDIA_TYPE;

        is_entry = 1;

        /* parse the incoming PUT body */
        if ((status = ap_xml_parse_input(r, &(entry.xml))) != OK)
            return server_error(r, status, "Can't parse XML PUT body");

    } else if (MATCHER(pub->patterns->media_object, r->uri) == 0) {
        is_entry = 0;
    } else {
        apr_table_set(r->headers_out, "Allow", "GET, HEAD");
        return HTTP_METHOD_NOT_ALLOWED;
    }

    /* Sigh.  We can create entries without locking, I'm pretty sure, but
     *  I think you can only have one thread/process at a time looking at
     *  the ETag and doing the PUT.
     */
    status = apr_global_mutex_create(&mutex, r->filename, APR_LOCK_DEFAULT,
                                     r->pool);
    if (status != APR_SUCCESS)
        return server_error(r, status, "Can't create mutex for '%s' to do PUT",
                            r->filename);

    /* I would rather use global_mutex_trylock and simply return if I can't
     *  get the lock, but that turns out not to be supported on certain
     *  platforms.
     */
    status = apr_global_mutex_lock(mutex);
    if (status != APR_SUCCESS) {
        apr_global_mutex_destroy(mutex);
        return server_error(r, status, "Can't lock '%s' for PUT", r->filename);
    }

    /* ETags */
    /* It turns out that make_etag depends on having the inode, size, and
     *  mtime filled in, which in the normal processing cycle, they aren't
     *  at this point.
     */
    /* Apache generates weak etags during the first second of a file's life,
     *  and if the client fetched immediately after posting, may have one
     *  of those.  Based on the the language in 2616, a weak ETag match
     *  is good enough for us, so we de-weakify both the If-Match value
     *  and the file ETag.
     */
    status = apr_stat(&finfo, r->filename,
                      APR_FINFO_MTIME | APR_FINFO_INODE | APR_FINFO_SIZE, r->pool);
    r->mtime = r->finfo.mtime = finfo.mtime;
    r->finfo.size = finfo.size;
    r->finfo.inode = finfo.inode;
    etag = ap_make_etag(r, 0);
    if (strncmp(if_match, WEAK_PREFIX, strlen(WEAK_PREFIX)) == 0)
        if_match += strlen(WEAK_PREFIX);
    if (strncmp(etag, WEAK_PREFIX, strlen(WEAK_PREFIX)) == 0)
        etag += strlen(WEAK_PREFIX);
    if (strcmp(if_match, etag)) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "ETag mismatch on PUT (%s vs %s), rejected", if_match, etag);
        apr_global_mutex_unlock(mutex);
        apr_global_mutex_destroy(mutex);
        return HTTP_PRECONDITION_FAILED;
    }

    /* both put_entry & put_media need this */
    if (!is_extra) {
        entry.path = is_prefix(r->filename, pub->dir) + 1;
        entry.path += strlen(PUB_DIR) + 1;
        entry.path = apr_pstrdup(r->pool, entry.path);
    }

    if (is_entry)
        status = put_entry(r, pub, &entry);
    else if (is_extra)
        status = simple_put(r, pub);
    else
        status = put_media(r, pub, &entry);
    apr_global_mutex_unlock(mutex);
    apr_global_mutex_destroy(mutex);

    if (is_extra)
        return status;

    /* put a timestamp file in place; means we have to find today's
     *  YYYY/MM/DD directory for the collection
     */
    if (status == OK) {
        char * dir;
        char * linkname;
        char * filename = entry_filename(r->pool, &entry);

	dir = atom_join(r->pool, entry.coll_base, 
                        yyyymmdd_at(apr_time_now(), r->pool), NULL);
        if ((problem = atom_dir(dir, r->pool)))
            return server_error(r, APR_SUCCESS,
                                "Can't make directory '%s' for PUT: %s",
                                dir, problem);

        linkname = apr_psprintf(r->pool, "%s/%s%ld-%ld",
                                dir, LINK_PREFIX, random(), random());
        if ((problem = to_file(linkname, filename, r->pool)))
            return server_error(r, APR_SUCCESS,
                                "Can't create link file '%s' to store '%s': %s",
                                linkname, filename, problem);
    }

    /* rewrite the collection */
    problem = write_collection(r->pool, pub, entry.coll_base,
                               DEFAULT_ENTRIES_IN_FEED);
    if (problem)
        return server_error(r, APR_SUCCESS,
                            "Can't write collection in '%s' post-PUT: %s",
                            entry.coll_base, problem);

    /* update the public-facing html & feed & front-page if required */
    if ((entry.type_of_content != MEDIA_TYPE) &&
            MATCHER(pub->patterns->entries_space, r->uri) == 0)
        write_html_entry(r, &entry);

    return status;
}

static int delete(request_rec * r, pub_t * pub) {
    apr_status_t status;
    char * problem;
    char * coll_base;
    int html_removed = 0;

    /* may not be necessary */
    if (!atom_file_exists(r->filename, r->pool))
        return HTTP_NOT_FOUND;

    /* in Extras space, just nuke it.  Hope they meant it. */
    if (MATCHER(pub->patterns->extras_space, r->uri) == 0) {
        apr_finfo_t finfo;
        apr_status_t status;

        apr_stat(&finfo, r->filename, APR_FINFO_MIN, r->pool);
        if (finfo.filetype == APR_DIR) {
            problem = rm_rf(r->pool, r->filename);
            if (problem)
                return server_error(r, APR_SUCCESS,
                                    "failed to delete directory '%s'",
                                    r->filename);
        } else {
            status = apr_file_remove(r->filename, r->pool);
	    if (status != APR_SUCCESS) 
		return server_error(r, status,
				    "failed to delete file '%s'",
				    r->filename);
	}
	return HTTP_NO_CONTENT;
    }

    /* can't delete media resources directly */
    if (MATCHER(pub->patterns->media_object, r->uri) == 0) {
        apr_table_set(r->headers_out, "Allow", "GET, HEAD, PUT");
        return HTTP_METHOD_NOT_ALLOWED;
    }

    /* has to be an entry */
    if (MATCHER(pub->patterns->entry, r->uri) != 0) {
        apr_table_set(r->headers_out, "Allow", "GET, HEAD");
        return HTTP_METHOD_NOT_ALLOWED;
    }

    /* if it's a media-link entry, then find & remove the associated
     *  media resource, and the media-type file
     */
    if (MATCHER(pub->patterns->media_space, r->uri) == 0) {

        /* the bits is the same filename minus the trailing .atom */
        char * bits;
        bits = apr_pstrdup(r->pool, r->filename);
        *(rindex(bits, '.')) = 0;
        status = apr_file_remove(bits, r->pool);
        if (status != APR_SUCCESS)
            return server_error(r, status,
                                "Can't remove media resource at '%s'", bits);

        bits = apr_psprintf(r->pool, "%s.%s", bits, MEDIA_TYPE_SUFFIX);
        status = apr_file_remove(bits, r->pool);
        if (status != APR_SUCCESS)
            return server_error(r, status,
                                "Can't remove media resource at '%s'", bits);

    } else {
        /* not a media-link entry.  Thus there may be an HTML or text file 
	 *  to lose; but there may not be, if the content@type was none of
	 *  'html', 'xhtml', or 'text'.
	 * We could parse the entry and look at the content type
         *  to figure out the exact name, but why not just try deleting any
	 *  of them.
         */
	char * extensions[] = { ".html", ".text", ".xhtml", NULL };
	int i;
	char * base =  apr_pstrdup(r->pool, r->filename);
	*(rindex(base, '.')) = 0;

	for (i = 0; extensions[i] && !html_removed; i++) {
	    char * target = apr_pstrcat(r->pool, base, extensions[i], NULL);
	    status = apr_file_remove(target, r->pool);
	    html_removed = html_removed || (status == APR_SUCCESS);
	}
    }

    /* delete on a META will delete a whole publication.  I hope they
     *  really, really wanted to.
     */
    if (pub->type == META_PUB) {
        char * * namespaces;
        apr_xml_elem * child;
        apr_xml_doc * entry;
        char * sub_pub = NULL;
        char * tempname;

        /* find the meta by finding <link rel="publication" */
        status = xnarf(r->filename, &entry, r->pool);
        if (status != APR_SUCCESS)
            return server_error(r, status,
                                "Can't parse meta-pub entry '%s'", r->filename);

        namespaces = (char * *) entry->namespaces->elts;
        for (child = entry->root->first_child; child; child = child->next) {
            if (strcmp(namespaces[child->ns], ATOM_NS) == 0 &&
                    strcmp(child->name, "link") == 0) {
                apr_xml_attr * a;
		char * href = NULL;
		char * rel = "";

                for (a = child->attr; a; a = a->next) {
                    if (a->ns == APR_XML_NS_NONE) {
			if (strcmp(a->name, "rel") == 0)
			    rel = (char *) a->value;
			else if (strcmp(a->name, "href") == 0)
			    href = (char *) a->value;
		    }
                }
		if (strcmp(rel, "publication") == 0)
		    sub_pub = href;
            }
        }

        if (sub_pub == NULL)
            return server_error(r, APR_SUCCESS,
                                "Can't find <link rel='publication>' in metapub entry '%s'",
                                r->filename);

        /* that's the service doc, whatever/atom/pub.atomsvc.  Need to back
         *  up two steps for root directory.  Also, it's in URI space not
         *  directory spce
         */
        sub_pub = atom_join(r->pool, pub->dir, is_prefix(sub_pub, pub->path),
                            NULL);
        *(rindex(sub_pub, '/')) = 0; /* D/atom */
        *(rindex(sub_pub, '/')) = 0; /* D */

        /* now, move it aside while we're munging it, because this
         *  could take a while.  
         */
        tempname = temp_filename(r->pool, sub_pub);
        status = apr_file_rename(tempname, sub_pub, r->pool);
        if (status != APR_SUCCESS)
            return server_error(r, status, "Can't rename sub_pub '%s' to '%s'",
                                sub_pub, tempname);

        if ((problem = rm_rf(r->pool, tempname)))
            return server_error(r, APR_SUCCESS,
                                "Can't delete sub-pub '%s': %s", sub_pub,
                                problem);
    }

    /* Finally, we delete the actual target entry */
    if ((status = apr_file_remove(r->filename, r->pool)) != APR_SUCCESS)
        return server_error(r, status, "Can't remove file '%s'", r->filename);

    coll_base = find_coll_base(r, pub);
    if ((problem = write_collection(r->pool, pub, coll_base,
                                    DEFAULT_ENTRIES_IN_FEED)) != NULL)
        return server_error(r, APR_SUCCESS,
                            "Can't write collection in '%s': %s",
                            coll_base, problem);

    /* we did nuke some HTML.  Have to update feed & front page */
    if (html_removed)
        update_public_site(r, pub, coll_base);

    return HTTP_NO_CONTENT;
}

/********************* Utilities ***********************/

/* make a temp filename for 'filename' */
static char * temp_filename(apr_pool_t * pool, char * filename) {
    char * tempname = dir_of(pool, filename);
    return apr_psprintf(pool, "%s/%s%ld-%ld", tempname,
                        TEMP_PREFIX, random(), random());
}

/* entry_t utilities */
static char * entry_filename(apr_pool_t * pool, entry_t * e) {
    char * s = atom_join(pool, e->pub->dir, PUB_DIR, e->path, NULL);
    return apr_pstrcat(pool, s, ".atom", NULL);
}

static char * entry_uri(apr_pool_t * pool, char * pub_or_atom, entry_t * e) {
    char * s = atom_join(pool, e->pub->path, pub_or_atom, e->path, NULL);
    return apr_pstrcat(pool, s, ".atom", NULL);
}

static char * entry_extension(apr_pool_t * pool, entry_t * e) {
    switch (e->type_of_content) {
    case MEDIA_TYPE: return "";
    case HTML_TYPE: return ".html";
    case XHTML_TYPE: return ".xhtml";
    case TEXT_TYPE: return ".text";
    }
    return NULL;
}

static char * entry_href(apr_pool_t * pool, entry_t * e) {
    char * s;
    char * extension = entry_extension(pool, e);

    s = atom_join(pool, e->pub->path, PUB_DIR, e->path, NULL);
    return apr_pstrcat(pool, s, extension, NULL);
}

/* dir_of; make a copy of a path, and chop off the last segment */
static char * dir_of(apr_pool_t * pool, char * path) {
    char * s;
    s = apr_pstrdup(pool, path);
    *(rindex(s, '/')) = 0;
    return s;
}

/* rm -rf; recursively nuke a directory tree */
static char * rm_rf(apr_pool_t * pool, char * name) {
    apr_status_t status;
    apr_dir_t * dir;
    apr_finfo_t finfo;
    char * problem;

    status = apr_dir_open(&dir, name, pool);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool, "Can't open dir '%s' in rm_rf", name);

    while (apr_dir_read(&finfo, APR_FINFO_NAME | APR_FINFO_TYPE, dir) == APR_SUCCESS) {
        if (finfo.filetype == APR_REG) {
            char * fname = atom_join(pool, name, finfo.name, NULL);
            if (apr_file_remove(fname, pool) != APR_SUCCESS)
                return apr_psprintf(pool, "Can't remove file '%s' in rm_rf",
                                    fname);
        } else if (finfo.filetype == APR_DIR) {
            if (strcmp(finfo.name, ".") == 0 || strcmp(finfo.name, "..") == 0)
                continue;

            problem = rm_rf(pool, atom_join(pool, name, finfo.name, NULL));
            if (problem)
                return problem;
        }
    }
    apr_dir_close(dir);
    if (apr_dir_remove(name, pool) != APR_SUCCESS)
        return apr_psprintf(pool, "Can't delete '%s' in rm_rf", name);

    return NULL;
}

/* create a new publication in response to a POST to a metapub */
static char * create_sub_pub(request_rec * r, entry_t * e, char * * sub_uri) {
    pub_t sub;
    char * problem;
    char * filename;
    int i;
    char * base_dir = e->pub->dir;
    char * base_path = e->pub->path;

    /* now we need to find a home for the pub.  Ten tries max.  If you stupidly
     *  select a slug/title like "News" or "Pix", well, it might not work
     */
    filename = entry_filename(r->pool, e);
    filename = rindex(filename, '/') + 1;
    for (i = 0; i < 10; i++) {
        char * subdir = apr_psprintf(r->pool, "%s%02ld", METAPUB_SUBS,
                                     random() % 100);
        filename = atom_join(r->pool, base_dir, subdir, filename, NULL);

        if (atom_dir(filename, r->pool) == NULL)
            break;
    }
    if (i == 10)
        return apr_psprintf(r->pool,
                            "Name collision creating new publication '%s' in '%s'",
                            filename, base_path);

    /* build_pub only needs the path & dir filled in */
    sub.type = SUB_PUB;
    sub.dir = apr_pstrdup(r->pool, filename);
    sub.path = atom_join(r->pool, base_path,
                         is_prefix(sub.dir, base_dir), NULL);
    sub.author = e->pub->author;
    sub.title = e->pub->title;
    if ((problem = build_pub(&sub, r->pool, r->pool)))
        return apr_psprintf(r->pool, "Can't build newly-created pub '%s': %s",
                            sub.dir, problem);

    /* <link rel="publication".  Relative is good enough */
    *sub_uri = atom_join(r->pool, sub.path, ATOM_DIR, SERVICE_FILE, NULL);
    return NULL;
}

/* Log an error.  If the status is other than APR_SUCCESS, use it to
 *  enhance the error logging
 */
static int server_error(request_rec * r, apr_status_t status, char * fmt, ...) {
    va_list args;
    char * msg;

    if (status != APR_SUCCESS) {
        char buf[1000];
        char * msg = apr_strerror(status, buf, 1000);

        fmt = apr_pstrcat(r->pool, fmt, ": ", NULL);
        fmt = apr_pstrcat(r->pool, fmt, msg, NULL);
    }

    va_start(args, fmt);
    msg = apr_pvsprintf(r->pool, fmt, args);
    va_end(args);
    ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, msg);
    return HTTP_INTERNAL_SERVER_ERROR;
}

static char * element_text(apr_xml_elem * e, apr_pool_t * pool) {
    apr_text * text;
    char * t = "";
    for (text = e->first_cdata.first; text; text = text->next)
        t = apr_pstrcat(pool, t, text->text, NULL);
    return t;
}

static int is_text_construct(apr_xml_elem * e, utf8 * namespaces) {
    int i;
    static char * text_constructs[] = {
        "content", "title", "summary", "rights", NULL
    };

    if (e->ns == APR_XML_NS_NONE ||
            strcmp((char *) namespaces[e->ns], ATOM_NS) != 0)
        return 0;

    for (i = 0; text_constructs[i]; i++)
        if (strcmp(e->name, text_constructs[i]) == 0)
            return 1;

    return 0;
}

static int find_num(char * * a_p) {
    char * a = *a_p;

    int sofar = 0;
    while (isdigit(*a)) {
        sofar = (sofar * 10) + (*a - '0');
        a++;
    }
    if (*a == 0 || *a == '&') {
        *a_p = a;
        return sofar;
    } else
        return -1;
}

/* Have to be careful that this gets run once-only for each collection/entry
 */
static char * new_id(apr_pool_t * pool) {
    apr_uuid_t uuid;
    char buf[APR_UUID_FORMATTED_LENGTH + 1];
    apr_uuid_get(&uuid);
    apr_uuid_format(buf, &uuid);
    return apr_psprintf(pool, "urn:uuid:%s", buf);
}

/* find the collection directory to which r->uri belongs
 *  Say r->uri = pub->path + (pub|atom) + e + Entries + yyyymmdd + E + ".atom"
 *  then   dir = pub->dir + pub + e + Entries
 */
static char * find_coll_base(request_rec * r, pub_t * pub) {
    char * coll;
    char * path;
    int i;
    int slashes;

    /* stupid collection.
     * we have to bypass the pub prefix, then three steps: PUB_DIR,
     *  ENTRIES_DIR, and the collection name;
     */
    /* lose pub->path */
    coll = apr_pstrdup(r->pool, is_prefix(r->uri, pub->path) + 1);

    /* bypass pub or atom */
    if ((path = is_prefix(coll, ATOM_DIR)))
        coll = path + 1;
    else
        coll = is_prefix(coll, PUB_DIR) + 1;

    /* now we have e/entries/yyyy/mm/dd/filename.atom
     *  so we need to chop off the last four path segments
     */
    for (i = strlen(coll) - 1, slashes = 0; slashes < 4; i--)
        if (coll[i] == '/')
            slashes++;
    coll[i + 1] = 0;
    return atom_join(r->pool, pub->dir, PUB_DIR, coll, NULL);
}

/* Does the actual work of pumping out collection bytes.  Needs to have
 *  a bunch of stuff initialized at a higher level.
 */
static char * generate_collection(char * dirname, apr_pool_t * pool,
                                  pub_t * pub, void * writer,
                                  int start, int count) {
    char * id_name = atom_join(pool, dirname, ID_FILE, NULL);
    int entries_in_feed;
    char * entry_name;
    char * * entry_names;
    int i;
    void * iter;
    char * problem;
    char * prefix;
    char * id;

    /* Unique ID for the collection */
    if ((problem = atom_snarf(id_name, &id, pool)))
        return apr_psprintf(pool, "Can't read ID file '%s'", id_name);

    /* write the per-feed elements */
    atom_genx_start_elem(writer, ATOM_FEED_E);
    atom_genx_add_ns(writer, APP_NS_NUM);
    atom_genx_add_ns(writer, MOD_ATOM_NS_NUM);

    if (pub->type == META_PUB)
        atom_genx_add_attr(writer, META_A, "service");

    atom_genx_simple_elem(writer, ATOM_TITLE_E, get_title(pub, pool));

    /* dirname is where the collection goes.  'prefix' is the bits between
     *  D/pub/ and dirname.  E.g. "e/entries"
     */
    prefix = is_prefix(dirname, pub->dir) + 1 + strlen(PUB_DIR);
    atom_genx_start_elem(writer, ATOM_LINK_E);

    /* e.g. <link rel="alt" href="D/pub/e/entries/ */
    atom_genx_add_attr(writer, HREF_A,
                       atom_join(pool, pub->path, PUB_DIR, prefix, NULL));
    atom_genx_end_elem(writer);
    atom_genx_simple_elem(writer, ATOM_UPDATED_E,
                          atom_time_at(apr_time_now(), pool));
    atom_genx_start_elem(writer, ATOM_AUTHOR_E);
    atom_genx_simple_elem(writer, ATOM_NAME_E, get_author(pub, pool));
    atom_genx_end_elem(writer);
    atom_genx_simple_elem(writer, ATOM_ID_E, id);

    if ((problem = atom_iter_new(pool, dirname, &iter)))
        return apr_psprintf(pool, "Can't start iterator on '%s': %s", dirname,
                            problem);

    /* we have to save up the entry filenames first, because we don't know
     *  whether to write a <link rel="next" into the feed header until we know
     *  if we're going to get all the entries in the feed.
     */
    entry_names = (char * *) apr_pcalloc(pool, count * sizeof (char *));

    for (problem = atom_iter_next(iter, &entry_name), i = entries_in_feed = 0;
            entry_name && (!problem) && (entries_in_feed < (start + count));
            problem = atom_iter_next(iter, &entry_name), entries_in_feed++) {
        if (entries_in_feed >= start)
            entry_names[i++] = entry_name;
    }

    if (entries_in_feed == (start + count)) {
        char * uri = atom_join(pool, pub->path, is_prefix(dirname, pub->dir),
                               COLLECTION_FILE, NULL);
        atom_genx_start_elem(writer, ATOM_LINK_E);
        atom_genx_add_attr(writer, REL_A, "next");
        atom_genx_add_attr(writer, HREF_A,
                           apr_psprintf(pool, "%s?start=%d", uri, start + count));
        atom_genx_end_elem(writer);
    }

    /* Now we jam in the entries, by snarfing them one at a time
     *  in reverse chronological order and just jamming them into the
     *  feed as-is.
     */
    for (count = i, i = 0; i < count; i++) {
        char * entry_body;

        if ((problem = atom_snarf(entry_names[i], &entry_body, pool)))
            return apr_psprintf(pool, "Can't read entry '%s': %s", entry_names[i],
                                problem);

        if (atom_genx_send(writer, entry_body) != GENX_SUCCESS)
            return "Can't write to collection";
    }
    if (problem)
        return apr_psprintf(pool, "Error iterating collection '%s': %s", dirname,
                            problem);

    atom_genx_end_elem(writer);
    atom_genx_end_doc(writer);
    return NULL;
}

/* The dirname is where to write it, the entries in the collection will
 *  be in yyyy/mm/dd directories underneath.
 * The mutexing here is usually unnecessary - there are definitely
 *  some windows that happen if you get two simultaneous updates to the same
 *  collection, where you'd end up with a collection feed that didn't show
 *  one of them.  But the next update that comes along will clean up; remember,
 *  the feed is generated by reading the reality in the underlying filesystem.
 *  The mutexing is a potential performance concern, but I decided for 
 *  release 1.0 to lean towards safety rather than premature optimization.
 * It might be smarter to just set a flag saying "collection needs re-writing"
 *  and let that happen on the next GET.  In fact, all you need is a
 *  last-touched timestamp file sitting in the directory and the GET could
 *  check whether this is newer than the current collection
 */
static char * write_collection(apr_pool_t * pool,
                               pub_t * pub, char * dirname, int size) {
    char * filename = atom_join(pool, dirname, COLLECTION_FILE, NULL);
    char * tempname = temp_filename(pool, filename);

    apr_file_t * fp;
    char * problem, * ret;
    apr_status_t status;
    void * writer;
    apr_global_mutex_t * mutex;

    status = apr_global_mutex_create(&mutex, tempname, APR_LOCK_DEFAULT, pool);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool,
                            "Can't create mutex on '%s' for update", tempname);
    status = apr_global_mutex_lock(mutex);
    if (status != APR_SUCCESS) {
        apr_global_mutex_destroy(mutex);
        return apr_psprintf(pool, "Can't lock '%s' for update", tempname);
    }

    status = apr_file_open(&fp, tempname, APR_FOPEN_WRITE | APR_FOPEN_CREATE,
                           PERMS, pool);
    if (status != APR_SUCCESS) {
        apr_global_mutex_unlock(mutex);
        apr_global_mutex_destroy(mutex);
        return apr_psprintf(pool, "Can't open collection tempfile '%s'",
                            tempname);
    }

    writer = atom_genx_file_writer(pool, fp, &problem);
    if (!writer) {
        apr_global_mutex_unlock(mutex);
        apr_global_mutex_destroy(mutex);
        return apr_psprintf(pool, "Can't start XML writer: '%s'", problem);
    }

    ret = NULL;
    problem = generate_collection(dirname, pool, pub, writer, 0, size);
    if (problem)
        ret = apr_psprintf(pool, "Can't generate collection in '%s'", dirname);

    apr_file_close(fp);

    if (ret == NULL)
        if (apr_file_rename(tempname, filename, pool) != APR_SUCCESS)
            ret = apr_psprintf(pool, "Can't rename '%s' to '%s'",
                               tempname, filename);
    apr_global_mutex_unlock(mutex);
    apr_global_mutex_destroy(mutex);
    return ret;
}

/* This is a PUT to a rel="edit-media" URI, i.e the raw bits.
 */
static int put_media(request_rec * r, pub_t * pub, entry_t * mle) {
    int status;

    if (pub->type == META_PUB)
        return HTTP_NOT_FOUND;

    mle->pub = pub;
    mle->coll_base = find_coll_base(r, pub);

    /* compute directories and names
     *  mle->path is              /m/Fm/yyyy/mm/dd/X
     *  edit_media URI is         pub->path + /atom + mle->path
     *  bits (r->filename) are in pub->dir  + /pub + mle->path
     *   (also content@src URI)x
     *  MLE URI is                pub->path + /atom + mle->path + ".atom"
     *  MLE file is               pub->dir + /pub + mle->path + ".atom"
     */
    if ((status = replace_media(r, mle)) != OK)
        return status;

    /* build and save a Media Link Entry for the bits */
    if ((status = update_mle(r, mle)) != OK)
        return status;

    return OK;
}

/* build an empty DOM with all the namespaces set up and so on */
static apr_xml_doc * make_mle_shell(apr_pool_t * pool) {
    char * * ns_here;
    apr_xml_doc * doc;
    int atom_ns = 1;

    doc = (apr_xml_doc *) apr_pcalloc(pool, sizeof (apr_xml_doc));
    doc->namespaces = apr_array_make(pool, 3, sizeof (char *));

    /* first slot hard-wired for DAV */
    apr_array_push(doc->namespaces);
    ns_here = (char * *) apr_array_push(doc->namespaces);
    *ns_here = ATOM_NS;
    ns_here = (char * *) apr_array_push(doc->namespaces);
    *ns_here = APP_NS;
    ns_here = (char * *) apr_array_push(doc->namespaces);
    *ns_here = XHTML_NS;

    doc->root = (apr_xml_elem *) apr_pcalloc(pool, sizeof (apr_xml_elem));
    doc->root->name = "entry";
    doc->root->ns = atom_ns;
    return doc;
}

/* mle stands for Media Link Entry.  The easiest way to do this is to build it
 *  in XML so we can just pass it to write_entry to persist it.
 */
static int save_mle(request_rec * r, entry_t * mle) {
    char * mtime = atom_time_at(r->request_time, r->pool);
    apr_xml_elem * root;
    int atom_ns = 1;
    int app_ns = 2;
    int xhtml_ns = 3;
    char * media_type;
    char * title;
    apr_xml_elem * child;
    char * tempname;
    char * target;
    char * problem;
    pub_t * pub = mle->pub;
    char * bits_uri, * mle_uri, * src;
    apr_status_t status;

    mle_uri = atom_join(r->pool, pub->path, ATOM_DIR, mle->path, NULL);
    mle_uri = apr_pstrcat(r->pool, mle_uri, ".atom", NULL);
    mle->xml = make_mle_shell(r->pool);
    root = mle->xml->root;

    insert_simple_child(r, root, atom_ns, "updated", mtime);
    insert_simple_child(r, root, app_ns, "edited", mtime);
    insert_simple_child(r, root, atom_ns, "id", mle->id);
    media_type = (char *) apr_table_get(r->headers_in, "Content-Type");
    title = apr_psprintf(r->pool, "%s: %d bytes at %s",
                         media_type, mle->size, mtime);
    insert_simple_child(r, root, atom_ns, "title", title);
    child = insert_simple_child(r, root, atom_ns, "author", NULL);
    insert_simple_child(r, child, atom_ns, "name", get_author(pub, r->pool));

    /* don't need an rel="alt' link */
    child = insert_simple_child(r, root, atom_ns, "link", NULL);
    insert_attr(r, child, "rel", "edit");
    insert_attr(r, child, "href", mle_uri);

    /* <link rel="edit-media" */
    child = insert_simple_child(r, root, atom_ns, "link", NULL);
    insert_attr(r, child, "rel", "edit-media");
    bits_uri = atom_join(r->pool, pub->path, ATOM_DIR, mle->path, NULL);
    insert_attr(r, child, "href", bits_uri);

    /* construct a nice summary */
    child = insert_simple_child(r, root, atom_ns, "summary", NULL);
    insert_attr(r, child, "type", "xhtml");
    child = insert_simple_child(r, child, xhtml_ns, "div", NULL);
    insert_simple_child(r, child, xhtml_ns, "p",
                        apr_psprintf(r->pool, "Media-type: '%s'", media_type));
    insert_simple_child(r, child, xhtml_ns, "p",
                        apr_psprintf(r->pool, "Size: '%d'", mle->size));
    insert_simple_child(r, child, xhtml_ns, "p",
                        apr_psprintf(r->pool, "Posted-at: '%s'", mtime));

    /* content and we're done */
    child = insert_simple_child(r, root, atom_ns, "content", NULL);
    insert_attr(r, child, "type", media_type);

    /* make a pub/-subtree version of the URI */
    src = atom_join(r->pool, pub->path, PUB_DIR, mle->path, NULL);
    insert_attr(r, child, "src", src);

    /* blast it into a tempfile */
    tempname = atom_join(r->pool, pub->dir, PUB_DIR, mle->path, NULL);
    tempname = temp_filename(r->pool, tempname);
    if ((problem = write_entry(r, mle->xml, tempname)))
        return server_error(r, APR_SUCCESS,
                            "Can't write Media Link Entry into '%s': %s",
                            tempname, problem);

    /* move it to its final home */
    target = atom_join(r->pool, pub->dir, PUB_DIR, mle->path, NULL);
    target = apr_pstrcat(r->pool, target, ".atom", NULL);
    if ((status = apr_file_rename(tempname, target, r->pool)) != APR_SUCCESS)
        return server_error(r, status,
                            "Can't rename from '%s' to '%s' to save MLE",
                            tempname, target);

    return OK;
}

/* mle stands for Media Link Entry.  We are updating a media object and
 *  thus its corresponding MLE.  All we do is adjust some dates.
 */
static int update_mle(request_rec * r, entry_t * mle) {
    apr_status_t status;
    char * filename;
    apr_xml_doc * existing;
    apr_xml_elem * id;

    /* get the ID from the old MLE */
    filename = apr_pstrcat(r->pool, r->filename, ".atom", NULL);
    status = xnarf(filename, &existing, r->pool);
    if (status != APR_SUCCESS)
        return server_error(r, status,
                            "Can't parse existing MLE file '%s'", filename);

    id = atom_xml_child(existing, existing->root, ATOM_NS, "id");
    mle->id = element_text(id, r->pool);
    return save_mle(r, mle);
}

/* new MLE, hence new ID.
 */
static int save_new_mle(request_rec * r, entry_t * mle) {
    mle->id = new_id(r->pool);
    return save_mle(r, mle);
}

static char * absolutize(char * relative, request_rec * r) {
    if (r->parsed_uri.port_str != NULL)
        return atom_join(r->pool,
                         apr_psprintf(r->pool, "http://%s:%s", r->hostname,
                                      r->parsed_uri.port_str),
                         relative, NULL);
    else
        return atom_join(r->pool,
                         apr_psprintf(r->pool, "http://%s", r->hostname),
                         relative, NULL);
}

/* Posting a media object.  We have to
 * - find a place for the bits
 * - remember the media type so we can look it up later
 * - create a synthetic atom media link entry in the media collection
 * - update the collection feed for the media collection
 */
static int post_media(request_rec * r, pub_t * pub) {
    entry_t mle;
    int status;
    char * mle_filename;
    char * mle_body;
    char * problem;
    char * absolute;

    if (pub->type == META_PUB)
        return HTTP_NOT_FOUND;

    /* save the bits */
    mle.pub = pub;
    mle.xml = NULL;
    if ((status = save_new_media(r, &mle)) != OK)
        return status;

    /* build and save a Media Link Entry for the bits */
    if ((status = save_new_mle(r, &mle)) != OK)
        return status;

    /* read back the mle */
    mle_filename = atom_join(r->pool, pub->dir, PUB_DIR, mle.path, NULL);
    mle_filename = apr_pstrcat(r->pool, mle_filename, ".atom", NULL);
    if ((problem = atom_snarf(mle_filename, &mle_body, r->pool)))
        return server_error(r, APR_SUCCESS, "Can't snarf '%s': %s",
                            mle_filename, problem);

    /* set up the headers */
    absolute = absolutize(entry_uri(r->pool, ATOM_DIR, &mle), r);
    apr_table_set(r->headers_out, "Location", absolute);

    /* apr_table_set(r->headers_out, "Content-Location", absolute);
     * XXX can't do the above until we have the ETag
     */
    ap_set_content_type(r, "application/atom+xml;type=entry");
    r->status = HTTP_CREATED;

    /* stuff the MLE back down the pipe to the client */
    ap_rputs(mle_body, r);

    /* re-write the collection */
    mle.coll_base = dir_of(r->pool, r->filename);
    if ((problem = write_collection(r->pool, pub, mle.coll_base,
                                    DEFAULT_ENTRIES_IN_FEED)))
        return server_error(r, APR_SUCCESS,
                            "Can't write media collection into '%s': %s",
                            mle.coll_base, problem);

    return OK;
}

static int find_entry_fields(char * name, int atom_ns,
                             entry_t * entry, apr_array_header_t * fields) {
    apr_xml_elem * child;
    apr_xml_elem * * new_field;

    for (child = entry->xml->root->first_child; child; child = child->next)
        if (child->ns == atom_ns && strcmp(child->name, name) == 0) {
            new_field = (apr_xml_elem * *) apr_array_push(fields);
            *new_field = child;
        }

    return fields->nelts;
}

/* update an entry in place
 * It's remarkably similar to initial entry creation, except for we
 *  don't change any of the URIs, and we re-use the unique ID from
 *  the entry that's already there.  So you could PUT a version with
 *  a completely new title and no similarity with the existing content.
 * entry has the entry->xml DOM filled in, that's all
 */
static int put_entry(request_rec * r, pub_t * pub, entry_t * e) {
    apr_status_t status;
    apr_xml_doc * existing;
    char * problem;
    char * tempname;
    char * filename;
    apr_xml_elem * child;

    status = xnarf(r->filename, &existing, r->pool);
    if (status != APR_SUCCESS)
        return server_error(r, status,
                            "Can't parse entry file '%s'", r->filename);

    /* pull the ID out of the existing child */
    child = atom_xml_child(existing, existing->root, ATOM_NS, "id");
    e->id = element_text(child, r->pool);

    /* need to know the type */
    child = atom_xml_child(existing, existing->root, ATOM_NS, "content");
    e->type_of_content = atom_text_construct_type(child);

    /* but we're putting an entry, so r->filename ends with ".atom" */
    *(rindex(e->path, '.')) = 0;
    e->pub = pub;
    e->coll_base = find_coll_base(r, pub);

    /* Fix up as required (ID, dates, links, etc) */
    if ((problem = prepare_entry(r, e, NULL)) != NULL) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Problem preparing entry via PUT: %s", problem);
        return HTTP_FORBIDDEN;
    }

    /* write it to a temp file */
    filename = entry_filename(r->pool, e);
    tempname = temp_filename(r->pool, filename);

    if ((problem = write_entry(r, e->xml, tempname)) != NULL)
        return server_error(r, APR_SUCCESS,
                            "Can't write PUT entry into '%s': %s",
                            tempname, problem);

    /* put it where it belongs */
    status = apr_file_rename(tempname, filename, r->pool);
    if (status != APR_SUCCESS)
        return server_error(r, status, "Can't rename from '%s' to '%s' for PUT",
                            tempname, filename);

    /* there's more work to do, but this is all we need to do while
     *  the entry's locked.
     */
    return OK;
}

/* Dump the contents of an atom entry into the HTML front page
 */
static void entry_to_fp(request_rec * r, pub_t * pub,
                        apr_xml_elem * entry, apr_xml_doc * doc,
                        genxWriter gw) {
    apr_xml_elem * title = NULL, * summary = NULL, * content = NULL,
	* author = NULL;
    char * link = NULL;
    char * wanted[] = {
	"title", "summary", "content", "author", NULL
    };
    apr_xml_elem * * children[] = {
	&title, &summary, &content, &author
    };

    if (is_draft(r->pool, doc))
	return;

    atom_xml_children(doc, entry, ATOM_NS, wanted, children);
    link = atom_xml_link_href(doc, entry, "alternate");

    genxStartElementLiteral(gw, NULL, (utf8) "div");
    genxAddAttributeLiteral(gw, NULL, (utf8) "class", (utf8) "atom-entry");
    genxAddCharacter(gw, '\n');
    genxStartElementLiteral(gw, NULL, (utf8) "div");
    genxAddAttributeLiteral(gw, NULL, (utf8) "class", (utf8) "atom-entry-title");
    if (link) {
        genxStartElementLiteral(gw, NULL, (utf8) "a");
        genxAddAttributeLiteral(gw, NULL, (utf8) "href", (utf8) link);
        atom_filter_text_construct(r->pool, doc, title, gw, 1);
        genxEndElement(gw);
    } else
        atom_filter_text_construct(r->pool, doc, title, gw, 1);
    genxEndElement(gw); /* <div class="atom-entry-title> */
    genxAddCharacter(gw, '\n');

    /* use the summary if it was provided */
    if (!summary)
        summary = content;

    if (summary) {
        genxStartElementLiteral(gw, NULL, (utf8) "div");
        genxAddAttributeLiteral(gw, NULL, (utf8) "class", (utf8) "atom-entry-body");
        atom_filter_text_construct(r->pool, doc, summary, gw, 1);
        genxEndElement(gw); /* <div class="atom-entry-body"> */
        genxAddCharacter(gw, '\n');
    }

    if (author) {
        apr_xml_elem * name = atom_xml_child(doc, author, ATOM_NS, "name");
	
        if (name) {
            genxStartElementLiteral(gw, NULL, (utf8) "div");
            genxAddAttributeLiteral(gw, NULL, (utf8) "class",
                                    (utf8) "atom-entry-author");
	    genxAddText(gw, (utf8) element_text(name, r->pool));
            genxEndElement(gw); /* <div clss='atom-entry-author"> */
            genxAddCharacter(gw, '\n');
        }
    }
    genxEndElement(gw); /* <div class="atom-entry" */
}

/* HTML-ify a Person Construct */
static void htmlify_person(apr_xml_elem * person, genxWriter gw, int atom_ns,
                           apr_pool_t * pool) {
    char * name = "?", * uri = NULL;
    apr_xml_elem * child;
    for (child = person->first_child; child; child = child->next) {
        if (child->ns == atom_ns) {
            if (strcmp(child->name, "uri") == 0)
                uri = element_text(child, pool);
            else if (strcmp(child->name, "name") == 0)
                name = element_text(child, pool);
        }
    }

    if (uri) {
        genxStartElementLiteral(gw, NULL, (utf8) "a");
        genxAddAttributeLiteral(gw, NULL, (utf8) "href", (utf8) uri);
    }
    genxAddText(gw, (utf8) name);
    if (uri)
        genxEndElement(gw); /* </a> */
}

static void htmlify_text_construct(entry_t * entry, char * name, int atom_ns,
                                   apr_array_header_t * fields,
                                   genxWriter gw, apr_pool_t * pool) {
    apr_xml_elem * tc;
    int count = find_entry_fields(name, atom_ns, entry, fields);
    if (count != 1)
        return;
    tc = ((apr_xml_elem * *) fields->elts)[0];
    atom_filter_text_construct(pool, entry->xml, tc, gw, 1);
}

static void htmlify_date(entry_t * entry, char * name, int atom_ns,
                         genxWriter gw, apr_array_header_t * fields,
                         apr_pool_t * pool) {
    apr_xml_elem * d;
    int count = find_entry_fields(name, atom_ns, entry, fields);
    if (count != 1)
        return;

    genxStartElementLiteral(gw, NULL, (utf8) "div");
    genxAddAttributeLiteral(gw, NULL, (utf8) "class",
                            (utf8) apr_psprintf(pool, "atom-date-%s", name));
    d = ((apr_xml_elem * *) fields->elts)[0];
    genxAddText(gw, (utf8) element_text(d, pool));
    genxEndElement(gw); /* </div> */
    genxAddCharacter(gw, '\n');
}

static void run_e_template_element(genxWriter gw, apr_xml_doc * template,
                                   apr_xml_elem * elem,
                                   request_rec * r, entry_t * entry,
                                   int template_ns, int atom_ns) {
    apr_array_header_t * fields;
    apr_xml_attr * a;
    int count;

    if (elem->ns != template_ns) {
        /* no action required, just copy it through */
        apr_xml_elem * child;

        genxStartElementLiteral(gw, NULL, (utf8) elem->name);
        for (a = elem->attr; a; a = a->next)
            genxAddAttributeLiteral(gw, NULL, (utf8) a->name, (utf8) a->value);
        atom_write_cdata(elem->first_cdata.first, gw);
        for (child = elem->first_child; child; child = child->next)
            run_e_template_element(gw, template, child, r, entry,
                                   template_ns, atom_ns);

        genxEndElement(gw);
        if (elem != template->root)
            atom_write_cdata(elem->following_cdata.first, gw);
        return;
    }

    fields = apr_array_make(r->pool, 10, sizeof (apr_xml_elem *));
    if (strcmp(elem->name, "cats") == 0) {

        /* put cat labels in a <ul>.  Use cat@label, otherwise cat@term */
        apr_xml_elem * * cats = (apr_xml_elem * *) fields->elts;
        int i;

        count = find_entry_fields("category", atom_ns, entry, fields);
        if (!count)
            return;
        genxStartElementLiteral(gw, NULL, (utf8) "ul");
        for (i = 0; i < count; i++) {
            utf8 label = NULL;

            for (a = cats[i]->attr; a; a = a->next) {
                if (a->ns == APR_XML_NS_NONE) {
                    if (strcmp(a->name, "label") == 0)
                        label = (utf8) a->value;
                    else if (strcmp(a->name, "term") == 0 &&
                            label == NULL)
                        label = (utf8) a->value;
                }
            }
            if (!label)
                return;

            genxStartElementLiteral(gw, NULL, (utf8) "li");
            genxAddAttributeLiteral(gw, NULL, (utf8) "class", (utf8) "atom-cat");
            genxStartElementLiteral(gw, NULL, (utf8) "p");
            genxAddText(gw, label);
            genxEndElement(gw); /* </p> */
            genxEndElement(gw); /* </li> */
            genxAddCharacter(gw, '\n');
        }
        genxEndElement(gw); /* </ul> */
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "content") == 0) {
        htmlify_text_construct(entry, "content", atom_ns, fields, gw, r->pool);
    } else if (strcmp(elem->name, "contributors") == 0) {
        /* put contributors in a comma-separated list, hyperlinked names */
        apr_xml_elem * * contribs = (apr_xml_elem * *) fields->elts;
        count = find_entry_fields("contributor", atom_ns, entry, fields);
        int i;
        if (count == 0)
            return;
        genxStartElementLiteral(gw, NULL, (utf8) "div");
        genxAddAttributeLiteral(gw, NULL, (utf8) "class",
                                (utf8) "atom-contributors");
        for (i = 0; i < count; i++) {
            htmlify_person(contribs[i], gw, atom_ns, r->pool);
            if (i < (count - 1))
                genxAddText(gw, (utf8) ", ");
        }
        genxEndElement(gw); /* </div> */
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "entry-author") == 0) {
        apr_xml_elem * author;
        count = find_entry_fields("author", atom_ns, entry, fields);
        if (count != 1)
            return;

        genxStartElementLiteral(gw, NULL, (utf8) "div");
        genxAddAttributeLiteral(gw, NULL, (utf8) "class", (utf8) "atom-author");
        author = ((apr_xml_elem * *) fields->elts)[0];
        htmlify_person(author, gw, atom_ns, r->pool);
        genxEndElement(gw); /* </author> */
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "entry-script") == 0) {
        genxStartElementLiteral(gw, NULL, (constUtf8) "script");
        genxAddAttributeLiteral(gw, NULL, (utf8) "type", (utf8) "text/javascript");
        genxAddAttributeLiteral(gw, NULL, (utf8) "src",
                                (utf8) atom_join(r->pool, entry->pub->path,
                                                 PUB_DIR, EXTRAS_DIR, JS_DIR,
                                                 ENTRY_JS, NULL));
        genxEndElement(gw);
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "entry-stylesheet") == 0) {
        genxStartElementLiteral(gw, NULL, (constUtf8) "link");
        genxAddAttributeLiteral(gw, NULL, (utf8) "rel", (utf8) "stylesheet");
        genxAddAttributeLiteral(gw, NULL, (utf8) "href",
                                (utf8) atom_join(r->pool, entry->pub->path,
                                                 PUB_DIR, EXTRAS_DIR, CSS_DIR,
                                                 ENTRY_CSS, NULL));
        genxEndElement(gw);
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "front-author") == 0) {
    } else if (strcmp(elem->name, "front-links") == 0) {
    } else if (strcmp(elem->name, "front-script") == 0) {
    } else if (strcmp(elem->name, "front-stylesheet") == 0) {
    } else if (strcmp(elem->name, "generator") == 0) {
        genxComment(gw, (utf8) GENERATOR);
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "logo") == 0) {
    } else if (strcmp(elem->name, "pub-title") == 0) {
        genxAddText(gw, (utf8) get_title(entry->pub, r->pool));
    } else if (strcmp(elem->name, "published") == 0) {
        htmlify_date(entry, "published", atom_ns, gw, fields, r->pool);
    } else if (strcmp(elem->name, "rights") == 0) {
        htmlify_text_construct(entry, "rights", atom_ns, fields, gw, r->pool);
    } else if (strcmp(elem->name, "title") == 0) {
        htmlify_text_construct(entry, "title", atom_ns, fields, gw, r->pool);
    } else if (strcmp(elem->name, "updated") == 0) {
        htmlify_date(entry, "updated", atom_ns, gw, fields, r->pool);
    }
}

static void run_e_template(genxWriter gw, apr_xml_doc * template,
                           request_rec * r, entry_t * entry) {
    char * * t_namespaces = (char * *) template->namespaces->elts;
    char * * e_namespaces = (char * *) entry->xml->namespaces->elts;
    int t_ns;
    int a_ns;

    for (t_ns = 0; t_ns < template->namespaces->nelts; t_ns++)
        if (strcmp(t_namespaces[t_ns], MOD_ATOM_NS) == 0)
            break;
    for (a_ns = 0; a_ns < entry->xml->namespaces->nelts; a_ns++)
        if (strcmp(e_namespaces[a_ns], ATOM_NS) == 0)
            break;

    run_e_template_element(gw, template, template->root, r, entry, t_ns, a_ns);
}

/* Upon encountering <m:entries/>, switches state either from Writing to
 *  Done or Bypassing to Writing.
 */
static template_state_t run_fp_template_element(genxWriter gw, pub_t * pub,
                                                apr_xml_doc * template,
                                                apr_xml_elem * elem, int t_ns,
                                                request_rec * r,
                                                template_state_t state) {
    if (elem->ns != t_ns) {
        apr_xml_attr * a;
        apr_xml_elem * child;

        /* just copy it through and recurse */
        if (state == Writing) {
            genxStartElementLiteral(gw, NULL, (utf8) elem->name);
            for (a = elem->attr; a; a = a->next)
                genxAddAttributeLiteral(gw, NULL, (utf8) a->name, (utf8) a->value);
            atom_write_cdata(elem->first_cdata.first, gw);
        }
        for (child = elem->first_child; child; child = child->next) {
            state = run_fp_template_element(gw, pub, template, child, t_ns,
                                            r, state);
            if (state == Done)
                return state;
        }
        if (state == Writing) {
            genxEndElement(gw);
            if (elem != template->root)
                atom_write_cdata(elem->following_cdata.first, gw);
        }
        return state;
    }

    /* it's a template element; <m:entries> gets special handling */
    if (strcmp(elem->name, "entries") == 0) {
        if (state == Writing)
            return Done;
        else
            /* strictly, should only be Bypassing */
            return Writing;
    }

    if (state == Bypassing)
        return state;

    /* OK, this is a non-"entries" template element, we actually have to
     *  think about it and make some output.
     */
    if (strcmp(elem->name, "front-links") == 0) {
        genxStartElementLiteral(gw, NULL, (utf8) "link");
        genxAddAttributeLiteral(gw, NULL, (utf8) "rel", (utf8) "alternate");
        genxAddAttributeLiteral(gw, NULL, (utf8) "href",
                                (utf8) atom_join(r->pool, pub->path, PUB_DIR,
                                                 ENTRIES_DIR, ENTRIES_COLL,
                                                 FEED_FILE, NULL));
        genxEndElement(gw);
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "front-script") == 0) {
        genxStartElementLiteral(gw, NULL, (utf8) "script");
        genxAddAttributeLiteral(gw, NULL, (utf8) "type", (utf8) "text/javascript");
        genxAddAttributeLiteral(gw, NULL, (utf8) "src",
                                (utf8) atom_join(r->pool, pub->path,
                                                 PUB_DIR, JS_DIR,
                                                 ENTRIES_DIR, ENTRIES_COLL,
                                                 FRONT_PAGE_JS, NULL));
        genxEndElement(gw);
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "front-stylesheet") == 0) {
        genxStartElementLiteral(gw, NULL, (constUtf8) "link");
        genxAddAttributeLiteral(gw, NULL, (utf8) "rel", (utf8) "stylesheet");
        genxAddAttributeLiteral(gw, NULL, (utf8) "href",
                                (utf8) atom_join(r->pool, pub->path,
                                                 PUB_DIR, CSS_DIR,
                                                 FRONT_PAGE_CSS, NULL));
        genxEndElement(gw);
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "generator") == 0) {
        genxComment(gw, (utf8)
                    "Brought to you by the awesome power of XML, "
                    "the Atom Publishing Protocol, the Apache Web server, "
                    "and mod_atom.");
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "pub-author") == 0) {
        genxStartElementLiteral(gw, NULL, (utf8) "div");
        genxAddAttributeLiteral(gw, NULL, (utf8) "class", (utf8) "atom-author");
        genxAddText(gw, (utf8) get_author(pub, r->pool));
        genxEndElement(gw); /* </author> */
        genxAddCharacter(gw, '\n');
    } else if (strcmp(elem->name, "pub-title") == 0) {
        genxAddText(gw, (utf8) get_title(pub, r->pool));
    } else if (strcmp(elem->name, "updated") == 0) {
        genxStartElementLiteral(gw, NULL, (utf8) "div");
        genxAddAttributeLiteral(gw, NULL, (utf8) "class",
                                (utf8) "atom-date-updated");
        genxAddText(gw, (utf8) atom_time_at(apr_time_now(), r->pool));
        genxEndElement(gw);
        genxAddCharacter(gw, '\n');
    }
    return state;
}

static void run_fp_template(genxWriter gw, pub_t * pub, apr_xml_doc * template,
                            request_rec * r, int phase) {
    char * * t_namespaces = (char * *) template->namespaces->elts;
    int t_ns;
    template_state_t state;

    for (t_ns = 0; t_ns < template->namespaces->nelts; t_ns++)
        if (strcmp(t_namespaces[t_ns], MOD_ATOM_NS) == 0)
            break;

    switch (phase) {
    case 1: state = Writing;
        break;
    case 2: state = Bypassing;
        break;
    default: return;
    }
    run_fp_template_element(gw, pub, template, template->root, t_ns, r, state);
}

/* something has happened to an entry which has a corresponding HTML
 *  version.  Thus, the public feed and front-page for that collection
 *  need to be updated.
 */
static void update_public_site(request_rec * r, pub_t * pub, char * coll_base) {
    apr_file_t * html_fp;
    apr_file_t * feed_fp;
    char * html_tmp;
    char * html_fn;
    char * feed_tmp;
    char * feed_fn;
    apr_status_t status;
    void * writer;
    genxWriter html_gw;
    char * problem;
    char * id_name;
    char * id;
    char * uri;
    int count;
    char * coll_fn;
    apr_xml_doc * coll;
    apr_xml_elem * entry;
    char * * namespaces;
    char * template_fn;
    apr_xml_doc * template;

    if (!is_html_enabled(r->pool, pub))
	return;

    html_tmp = apr_psprintf(r->pool, "%s/%s%ld-%ld", coll_base,
                            TEMP_PREFIX, random(), random());
    html_fn = atom_join(r->pool, coll_base, "index.html", NULL);
    feed_tmp = apr_psprintf(r->pool, "%s/%s%ld-%ld", coll_base,
                            TEMP_PREFIX, random(), random());
    feed_fn = atom_join(r->pool, coll_base, FEED_FILE, NULL);

    /* Unique ID for the feed */
    id_name = atom_join(r->pool, coll_base, ID_FILE, NULL);
    if ((problem = atom_snarf(id_name, &id, r->pool))) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't read ID file '%s'", id_name);
        return;
    }

    /* open tempfiles which will become index.html and feed */
    status = apr_file_open(&html_fp, html_tmp,
			   APR_FOPEN_WRITE | APR_FOPEN_CREATE, PERMS, r->pool);
    if (status != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't create tempfile for front page '%s'", html_tmp);
        return;
    }
    status = apr_file_open(&feed_fp, feed_tmp,
			   APR_FOPEN_WRITE | APR_FOPEN_CREATE, PERMS, r->pool);
    if (status != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't create tempfile for public feed '%s'", feed_tmp);
        return;
    }

    /* get index.html going */
    writer = atom_genx_file_writer(r->pool, html_fp, &problem);
    if (!writer) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't make XML writer for '%s': '%s'", html_tmp, problem);
        return;
    }
    html_gw = atom_genx_get_writer(writer);

    // we'll write out the first phase of the front-page template (up until
    //  it hits <m:entries/>)
    template_fn = atom_join(r->pool, pub->dir, PUB_DIR, EXTRAS_DIR,
                            TEMPLATES_DIR, FRONT_PAGE_TEMPLATE, NULL);
    if (xnarf(template_fn, &template, r->pool) != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't parse front page template '%s'", template_fn);
        return;
    }
    run_fp_template(html_gw, pub, template, r, 1);

    /* get the feed going */
    writer = atom_genx_file_writer(r->pool, feed_fp, &problem);
    if (!writer) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't make XML writer for '%s': '%s'", feed_tmp, problem);
        return;
    }

    /* write the per-feed elements */
    atom_genx_start_elem(writer, ATOM_FEED_E);
    atom_genx_add_ns(writer, APP_NS_NUM);
    atom_genx_add_ns(writer, XHTML_NS_NUM);
    atom_genx_nl(writer);
    atom_genx_simple_elem(writer, ATOM_TITLE_E, get_title(pub, r->pool));
    atom_genx_start_elem(writer, ATOM_LINK_E);
    atom_genx_add_attr(writer, REL_A, "self");
    atom_genx_add_attr(writer, HREF_A, "");
    atom_genx_end_elem(writer);
    atom_genx_nl(writer);
    atom_genx_simple_elem(writer, ATOM_GENERATOR_E, GENERATOR);

    /* figure out the URI of the html page */
    atom_genx_start_elem(writer, ATOM_LINK_E);
    atom_genx_add_attr(writer, REL_A, "alternate");
    uri = atom_join(r->pool, pub->path, is_prefix(coll_base, pub->dir), NULL);
    atom_genx_add_attr(writer, HREF_A, apr_psprintf(r->pool, "%s/", uri));
    atom_genx_end_elem(writer);
    atom_genx_nl(writer);

    atom_genx_simple_elem(writer, ATOM_UPDATED_E,
                          atom_time_at(apr_time_now(), r->pool));
    atom_genx_start_elem(writer, ATOM_AUTHOR_E);
    atom_genx_simple_elem(writer, ATOM_NAME_E, get_author(pub, r->pool));
    atom_genx_end_elem(writer);
    atom_genx_nl(writer);
    atom_genx_simple_elem(writer, ATOM_ID_E, id);

    coll_fn = atom_join(r->pool, coll_base, COLLECTION_FILE, NULL);
    status = xnarf(coll_fn, &coll, r->pool);
    if (status != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't parse collection file '%s'", coll_fn);
        return;
    }

    /* OK, now we'll run through the entries from the feed */
    namespaces = (char * *) coll->namespaces->elts;
    for (count = 0, entry = coll->root->first_child;
            entry && (count < DEFAULT_ENTRIES_IN_FEED);
            entry = entry->next) {
        text_construct_type_t type = TEXT_TYPE;
	apr_xml_elem * content;

        /* skip non-entries */
        if (entry->ns == APR_XML_NS_NONE ||
                strcmp(namespaces[entry->ns], ATOM_NS) != 0 ||
                strcmp(entry->name, "entry") != 0)
            continue;

        /* for each atom:entry in the feed */
        /* if the type on its content is other than MEDIA_TYPE, we'll keep it */
	content = atom_xml_child(coll, entry, ATOM_NS, "content");
	if (content)
	    type = atom_text_construct_type(content);

        if (type == MEDIA_TYPE)
            continue;

        /* OK, this entry goes into the HTML */
        entry_to_fp(r, pub, entry, coll, html_gw);

        /* into public feed */
        problem = write_element(coll, entry,
                                atom_genx_get_writer(writer), 1, r->pool);
        if (problem) {
            ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                         "Can't write entry into feed in '%s': %s", coll_base,
                         problem);
        }
    }

    /* write out the part of the front page after <m:entries/> */
    run_fp_template(html_gw, pub, template, r, 2);
    apr_file_close(html_fp);
    apr_file_rename(html_tmp, html_fn, r->pool);

    /* finish off the feed */
    atom_genx_end_elem(writer);
    atom_genx_end_doc(writer);
    apr_file_close(feed_fp);
    apr_file_rename(feed_tmp, feed_fn, r->pool);
}

/* Does an app:draft appear as a child of an app:control which is a child
 *  of the atom:entry?
 */
static int is_draft(apr_pool_t * pool, apr_xml_doc * doc) {
    apr_xml_elem * control;
    char * draft_val = "no";

    if ((control = atom_xml_child(doc, doc->root, APP_NS, "control"))) {
	apr_xml_elem * draft;
	if ((draft = atom_xml_child(doc, control, APP_NS, "draft")))
	    draft_val = element_text(draft, pool);
    }

    return !strcmp(draft_val, "yes");
}

/* is a pub HTML-enabled? */
static int is_html_enabled(apr_pool_t * pool, pub_t * pub) {
    return atom_file_exists(pub->options_html_file, pool);
}

/* create the HTML version of an entry; return true/false indicating
 *  whether HTML was in fact written
 */
static void write_html_entry(request_rec * r, entry_t * e) {
    char * filename;
    char * tempname;
    apr_status_t status;
    apr_file_t * fp;

    genxWriter gw;
    void * writer;
    char * problem;
    apr_xml_elem
            * title = NULL, * summary = NULL, * rights = NULL, * content = NULL;

    apr_xml_doc * template;
    char * template_fn;
    char * wanted[] = { "content", "title", "summary", "rights", NULL };
    apr_xml_elem * * children[] = { &content, &title, &summary, &rights };

    char * extension = entry_extension(r->pool, e);

    /* Two reasons for not writing HTML:
     * 1. app:draft is set in the entry
     * 2. the file x/options/html does not exist
     */
    if (is_draft(r->pool, e->xml))
	return;
    if (!is_html_enabled(r->pool, e->pub))
	return;

    /* first, we have to find the text constructs */
    atom_xml_children(e->xml, e->xml->root, ATOM_NS, wanted, children);

    filename = atom_join(r->pool, e->pub->dir, PUB_DIR, e->path, NULL);
    filename = apr_pstrcat(r->pool, filename, extension, NULL);
    tempname = dir_of(r->pool, filename);

    /* now the tempname is the the directory name.  Is it there? */
    if ((problem = atom_dir(tempname, r->pool))) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't make HTML dir '%s': %s", tempname, problem);
        return;
    }

    /* the tempname we'll actually write into */
    tempname = temp_filename(r->pool, filename);
    status = apr_file_open(&fp, tempname,
                           APR_FOPEN_WRITE | APR_FOPEN_CREATE, PERMS, r->pool);
    if (status != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't create HTML file '%s'", tempname);
        return;
    }

    writer = atom_genx_file_writer(r->pool, fp, &problem);
    if (!writer) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't make XML writer for '%s': %s", tempname, problem);
        return;
    }
    gw = atom_genx_get_writer(writer);

    template_fn = atom_join(r->pool, e->pub->dir, PUB_DIR, EXTRAS_DIR,
                            TEMPLATES_DIR, ENTRY_TEMPLATE, NULL);
    if (xnarf(template_fn, &template, r->pool) != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't parse entry template '%s'", template_fn);
        return;
    }
    run_e_template(gw, template, r, e);

    /* rename tempfile to final target */
    if (apr_file_rename(tempname, filename, r->pool) != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't rename '%s' to '%s'", tempname, filename);
    }

    update_public_site(r, e->pub, e->coll_base);
}

/* Create an entry, and generate related URIs */
static int post_entry(request_rec * r, entry_t * e) {
    int status;
    char * problem;
    char * entry_body = NULL;
    char * absolute;
    char * filename;

    /* Try to suck it up */
    if ((status = ap_xml_parse_input(r, &(e->xml))) != OK) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't parse XML POST body");
        return status;
    }

    /* Blast it into a file */
    status = save_new_entry(r, e);
    if (status != OK)
        return status;

    /* read the file back */
    filename = entry_filename(r->pool, e);
    if ((problem = atom_snarf(filename, &entry_body, r->pool)))
        return server_error(r, APR_SUCCESS, "Can't snarf '%s': %s",
                            filename, problem);

    /* headers */
    absolute = absolutize(entry_uri(r->pool, ATOM_DIR, e), r);
    apr_table_set(r->headers_out, "Location", absolute);

    /* XXX Can't do the following without an ETAG */
    /* apr_table_set(r->headers_out, "Content-Location", absolute); */

    ap_set_content_type(r, "application/atom+xml;type=entry");
    r->status = HTTP_CREATED;

    /* blast it down the pipe */
    ap_rputs(entry_body, r);

    /* update the collection */
    if ((problem = write_collection(r->pool, e->pub,
                                    e->coll_base,
                                    DEFAULT_ENTRIES_IN_FEED)))
        return server_error(r, APR_SUCCESS,
                            "Can't write collection in '%s': %s",
                            e->coll_base, problem);

    /* don't write it unless it's text, html, or xhtml */
    if (e->type_of_content != MEDIA_TYPE)
        write_html_entry(r, e);

    return OK;
}

/* Generate an atom-conformant timestamp */
static char * atom_time_at(apr_time_t when, apr_pool_t * pool) {
    char * buf = apr_pcalloc(pool, 100);
    apr_size_t time_len;
    apr_time_exp_t times;

    apr_time_exp_lt(&times, when);
    apr_strftime(buf, &time_len, 100, "%Y-%m-%dT%H:%M:%S%z", &times);

    /* I'm assuming there's a strftime incantation to do this? */
    buf[time_len] = buf[time_len - 1];
    buf[time_len - 1] = buf[time_len - 2];
    buf[time_len - 2] = ':';

    return buf;
}

/* Drop in a child element with no attributes and possibly content.
 */
static apr_xml_elem * insert_simple_child(request_rec * r,
                                          apr_xml_elem * parent,
                                          int ns_ind, char * type,
                                          char * content) {
    apr_xml_elem * e =
            (apr_xml_elem *) apr_pcalloc(r->pool, sizeof (apr_xml_elem));

    e->ns = ns_ind;
    e->name = type;
    e->parent = parent;
    e->next = parent->first_child;
    e->following_cdata = parent->first_cdata;
    if (content)
        insert_text_content(r, e, content);
    else
        parent->first_cdata.first = parent->first_cdata.last = NULL;
    parent->first_child = e;
    return e;
}

/* Drop #PCData into an element, replacing anything that might have been there
 */
static char * insert_text_content(request_rec * r, apr_xml_elem * e, char * t) {
    apr_text_header * text_h = apr_palloc(r->pool, sizeof (apr_text_header));
    apr_text * text = apr_palloc(r->pool, sizeof (apr_text));

    text->text = t;
    text->next = NULL;
    text_h->first = text_h->last = text;
    e->first_cdata = *text_h;
    return NULL;
}

/* Add an attribute to an element */
static void insert_attr(request_rec * r, apr_xml_elem * e,
                        char * name, char * value) {
    apr_xml_attr * attr;

    for (attr = e->attr; attr; attr = attr->next)
        if (strcmp(name, attr->name) == 0)
            break;

    if (attr == NULL) {
        attr = apr_pcalloc(r->pool, sizeof (apr_xml_attr));
        attr->ns = APR_XML_NS_NONE;
        attr->name = name;
        attr->next = e->attr;
        e->attr = attr;
    }
    attr->value = value;
}

text_construct_type_t atom_text_construct_type(apr_xml_elem * e) {
    char * type_name = "text";
    text_construct_type_t type = TEXT_TYPE;
    apr_xml_attr * attr;

    for (attr = e->attr; attr; attr = attr->next) {
        if (attr->ns == APR_XML_NS_NONE && strcmp(attr->name, "type") == 0)
            type_name = (char *) attr->value;
    }

    if (index(type_name, '/'))
        type = MEDIA_TYPE;
    else if (strcmp(type_name, "xhtml") == 0)
        type = XHTML_TYPE;
    else if (strcmp(type_name, "html") == 0)
        type = HTML_TYPE;
    else if (strcmp(type_name, "text") == 0)
        type = TEXT_TYPE;

    return type;
}

/* Entry validation/fix-up; return NULL or the decription of the problem
 * - must have a title
 * - add an author if we don't know it
 * - fix up atom:updated, atom:id, app:edited
 */
static char * prepare_entry(request_rec * r, entry_t * e, char * sub_uri) {
    char * * ns_names = (char * *) e->xml->namespaces->elts;
    int i;
    int atom_ns = -1, app_ns = -1, xhtml_ns = -1;
    apr_xml_elem * updated = NULL, * author = NULL,
            * id = NULL, * edited = NULL, * alt_link = NULL, * edit_link = NULL;
    apr_xml_elem * root = e->xml->root;
    char * mtime;
    char * wanted[] = { "author", "updated", "id", "edited", NULL };
    apr_xml_elem * * children[] = { &author, &updated, &id, &edited };

    for (i = 0; i < e->xml->namespaces->nelts; i++) {
        if (!strcmp(ns_names[i], ATOM_NS))
            atom_ns = i;
        else if (!strcmp(ns_names[i], APP_NS))
            app_ns = i;
        else if (!strcmp(ns_names[i], XHTML_NS))
            xhtml_ns = i;
    }
    /* probably no app stuff in the incoming */
    if (app_ns == -1) {
        char * * ns_here;
        app_ns = e->xml->namespaces->nelts;
        ns_here = (char * *) apr_array_push(e->xml->namespaces);
        *ns_here = APP_NS;
        ns_names = (char * *) e->xml->namespaces->elts;
    }
    if (xhtml_ns == -1) {
        char * * ns_here;
        xhtml_ns = e->xml->namespaces->nelts;
        ns_here = (char * *) apr_array_push(e->xml->namespaces);
        *ns_here = XHTML_NS;
        ns_names = (char * *) e->xml->namespaces->elts;
    }

    if (root->ns != atom_ns || strcmp(root->name, "entry") != 0)
        /* XXX user_error */
        return "Root of doc received as an Entry was not atom:entry";

    atom_xml_children(e->xml, root, ATOM_NS, wanted, children);
    alt_link = atom_xml_link_el(e->xml, root, "alternate");
    edit_link = atom_xml_link_el(e->xml, root, "edit");

    mtime = atom_time_at(r->request_time, r->pool);
    if (updated == NULL)
        updated = insert_simple_child(r, root, atom_ns, "updated", NULL);
    insert_text_content(r, updated, mtime);
    if (edited == NULL)
        edited = insert_simple_child(r, root, app_ns, "edited", NULL);
    insert_text_content(r, edited, mtime);

    if (id == NULL)
        id = insert_simple_child(r, root, atom_ns, "id", NULL);
    insert_text_content(r, id, e->id);

    if (author == NULL) {
        author = insert_simple_child(r, root, atom_ns, "author", NULL);
        author = insert_simple_child(r, author, atom_ns, "name", NULL);
        insert_text_content(r, author, get_author(e->pub, r->pool));
    }

    /* we only put in a <link rel="alternate" if we've generated HTML */
    if (e->type_of_content != MEDIA_TYPE) {
        if (alt_link == NULL) {
            alt_link = insert_simple_child(r, root, atom_ns, "link", NULL);
            insert_attr(r, alt_link, "rel", "alternate");
        }
        insert_attr(r, alt_link, "href", entry_href(r->pool, e));
    }

    /* if we've created a sub-pub, need to put in a link for that */
    if (sub_uri) {
        alt_link = insert_simple_child(r, root, atom_ns, "link", NULL);
        insert_attr(r, alt_link, "rel", "publication");
        insert_attr(r, alt_link, "href", sub_uri);
    }

    if (edit_link == NULL) {
        edit_link = insert_simple_child(r, root, atom_ns, "link", NULL);
        insert_attr(r, edit_link, "rel", "edit");
    }
    insert_attr(r, edit_link, "href", entry_uri(r->pool, ATOM_DIR, e));

    return NULL;
}

/* write out an element of a document, then its children recursively */
static char * write_element(apr_xml_doc * doc, apr_xml_elem * e, genxWriter gw,
                            int filter, apr_pool_t * pool) {
    apr_xml_attr * attr;
    utf8 * ns_names = (utf8 *) doc->namespaces->elts;
    apr_xml_elem * child;
    apr_text * text;

    genxStartElementLiteral(gw, ns_names[e->ns], (utf8) e->name);
    if (e->lang)
        genxAddAttributeLiteral(gw, (utf8) XML_NS, (utf8) "lang", (utf8) e->lang);

    for (attr = e->attr; attr; attr = attr->next) {
        utf8 ns = (attr->ns == APR_XML_NS_NONE) ? NULL : ns_names[attr->ns];
        genxAddAttributeLiteral(gw, ns, (utf8) attr->name, (utf8) attr->value);
    }

    for (text = e->first_cdata.first; text; text = text->next)
        genxAddText(gw, (utf8) text->text);

    for (child = e->first_child; child; child = child->next) {
        char * problem;

        if (filter && is_text_construct(child, ns_names)) {
            genxStartElementLiteral(gw, ns_names[child->ns], (utf8) child->name);
            genxAddAttributeLiteral(gw, NULL, (utf8) "type", (utf8) "xhtml");
            genxStartElementLiteral(gw, (utf8) XHTML_NS, (utf8) "div");
            problem = atom_filter_text_construct(pool, doc, child, gw, 0);
            genxEndElement(gw); /* </div> */
            genxEndElement(gw); /* </child->name> */
            genxAddCharacter(gw, '\n');
        } else
            problem = write_element(doc, child, gw, filter, pool);

        if (problem)
            return problem;
    }

    genxEndElement(gw);

    for (text = e->following_cdata.first; text; text = text->next)
        genxAddText(gw, (utf8) text->text);

    return NULL;
}

/* persist an XML document */
static char * write_entry(request_rec * r, apr_xml_doc * doc, char * filename) {
    apr_status_t status;
    apr_file_t * fp;
    char * problem;
    void * writer;

    status = apr_file_open(&fp, filename,
                           APR_FOPEN_WRITE | APR_FOPEN_CREATE, PERMS, r->pool);
    if (status != APR_SUCCESS)
        return apr_psprintf(r->pool, "Failed to open '%s', status: %d",
                            filename, status);

    writer = atom_genx_file_writer(r->pool, fp, &problem);
    if (!writer)
        return apr_psprintf(r->pool, "Can't start XML writer: '%s'", problem);

    problem = write_element(doc, doc->root, atom_genx_get_writer(writer),
                            0, r->pool);
    atom_genx_end_doc(writer);
    apr_file_close(fp);
    if (problem)
        return problem;

    return NULL;
}

/* Compute a filename name for a new resource, based on the slug, or failing
 * that, the title of the entry, if provided, or failing that, a random number
 * Here's how we try to create new files without locking anything.
 *  We build a name from the title (or slug, if provided) and try to
 *  create that.  If there's a collision, we append a large random
 *  number to the name, and try again, twice.  If it still fails, give up.
 * When this returns, there's a window during which the file being created
 *  exists, but is empty.  Hmm.
 */
static char * new_resource_filename(request_rec * r, char * dir, entry_t * e) {
    char * slug = (char *) apr_table_get(r->headers_in, "Slug");
    char * filename;
    char * pathname;
    apr_status_t status;
    apr_file_t * fp;

    /* if no slug from user */
    if (!slug) {
        if (e->xml) {
            apr_xml_elem * title =
		atom_xml_child(e->xml, e->xml->root, ATOM_NS, "title");
	    if (title)
		slug = element_text(title, r->pool);
        }
    }

    /* if there's any kind of incoming title suggestion, try to use it */
    if (slug) {
        filename = decode_slug(slug, r->pool);

        pathname = atom_join(r->pool, dir, filename, NULL);
	pathname = apr_pstrcat(r->pool, pathname, ".atom", NULL);
        status = apr_file_open(&fp, pathname,
                               APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_EXCL,
                               PERMS, r->pool);

        if (status != APR_SUCCESS) {
            int i;
            for (i = 0; status != APR_SUCCESS && i < MAX_FILENAME_VARIATIONS; i++) {
		pathname = atom_join(r->pool, dir, filename, NULL);
                pathname = apr_psprintf(r->pool, "%s-%ld.atom",
					pathname, random());
                status = apr_file_open(&fp, pathname,
                                       APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_EXCL,
                                       PERMS, r->pool);
            }
            if (status == APR_SUCCESS) {
                filename = rindex(pathname, '/') + 1;
		*(rindex(filename, '.')) = 0;
	    }
        }
        if (status == APR_SUCCESS) {
            apr_file_close(fp);
            return filename;
        } else {
            ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                         "Can't create file starting with '%s/%s", dir, filename);
            return NULL;
        }
    }

    /* no slug from user, no title */
    filename = apr_psprintf(r->pool, "r-%ld-%ld", random(), random());
    pathname = atom_join(r->pool, dir, filename, NULL);

    status = apr_file_open(&fp, pathname,
                           APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_EXCL,
                           PERMS, r->pool);
    if (status == APR_SUCCESS) {
        apr_file_close(fp);
        return filename;
    }

    ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                 "Can't create file '%s", pathname);
    return NULL;
}

/* Figure out some entry characteristics
 */
static apr_status_t init_entry(request_rec * r, char * dir,
                               char * filename, entry_t * e) {
    char * path;
    apr_xml_elem * content;

    e->type_of_content = TEXT_TYPE; /* by default */

    /* have to do fish out the content element (if any) to figure out
     *  whether there's going to be a .html
     */
    if ((content = atom_xml_child(e->xml, e->xml->root, ATOM_NS, "content")))
	e->type_of_content = atom_text_construct_type(content);

    /* chop pub->dir and /pub/ off the front */
    path = is_prefix(dir, e->pub->dir) + 1;
    path += strlen(PUB_DIR) + 1;
    e->path = atom_join(r->pool, path, filename, NULL);

    /* called from a POST, so r->filename is the collection */
    e->coll_base = dir_of(r->pool, r->filename);

    return OK;
}

/* turn a string into a file; should be called "frans" or "un_snarf" */
static char * to_file(char * filename, char * data, apr_pool_t * pool) {
    apr_status_t status;
    apr_file_t * fp;
    apr_size_t written;

    status = apr_file_open(&fp, filename, APR_FOPEN_WRITE | APR_FOPEN_CREATE,
                           PERMS, pool);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool, "to_file: can't create '%s'", filename);

    status = apr_file_write_full(fp, (void *) data, strlen(data), &written);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool, "to_file: can't write to tempfile '%s'",
                            filename);
    apr_file_close(fp);
    return NULL;
}

/* turn an XML file into an XML DOM */
static int xnarf(char * filename, apr_xml_doc * * ret, apr_pool_t * pool) {
    apr_status_t status;
    apr_file_t * fp;
    apr_xml_parser * parser;

    status = apr_file_open(&fp, filename, APR_FOPEN_READ, PERMS, pool);
    if (status != APR_SUCCESS)
        return status;

    status = apr_xml_parse_file(pool, &parser, ret, fp, BUFSIZ);
    apr_file_close(fp);
    return status;
}

/* turn a file into a string */
char * atom_snarf(char * filename, char * * ret, apr_pool_t * pool) {
    apr_status_t status;
    apr_file_t * fp;
    apr_finfo_t finfo;
    char * buf;
    apr_size_t read_size;

    status = apr_stat(&finfo, filename, APR_FINFO_SIZE, pool);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool, "Snarf: Can't stat '%s'", filename);

    status = apr_file_open(&fp, filename, APR_FOPEN_READ, PERMS, pool);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool, "Snarf: Can't open '%s'", filename);

    read_size = finfo.size + 1;
    buf = apr_palloc(pool, read_size);
    if (buf == NULL)
        return apr_psprintf(pool, "Snarf: Can't allocate %ld bytes to read '%s'",
                            (long) read_size, filename);
    status = apr_file_read(fp, buf, &read_size);
    apr_file_close(fp);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool, "Snarf: Can't read %ld bytes from '%s'",
                            (long) finfo.size, filename);
    buf[read_size] = 0;
    *ret = buf;
    return NULL;
}

/* Store data arriving the body of a POST or PUT, without interpretation.
 *  There are a variety of server errors that can occur if you can't open
 *  or write the file or whatever.  There's one user error that can occur:
 *  the file is too large per ap_get_limit_req_body.  
 */
static int store_incoming_bits(request_rec * r, char * filename, int * size) {
    apr_file_t * fp;
    apr_status_t status;
    apr_bucket_brigade * brigade;
    int seen_eos;
    apr_size_t total_read;
    apr_off_t max;
    int ret = OK;

    status = apr_file_open(&fp, filename, APR_FOPEN_WRITE | APR_FOPEN_CREATE,
                           PERMS, r->pool);
    if (status != APR_SUCCESS)
        return server_error(r, status,
                            "Can't open file to write %s to write %s",
                            filename, r->uri);

    max = ap_get_limit_req_body(r);
    brigade = apr_brigade_create(r->pool, r->connection->bucket_alloc);

    seen_eos = 0;
    total_read = 0;

    do {
        apr_bucket * bucket;

        status = ap_get_brigade(r->input_filters, brigade, AP_MODE_READBYTES,
                                APR_BLOCK_READ, BUFSIZ);
        if (status != APR_SUCCESS)
            return server_error(r, status, "Can't get brigade.");

        for (bucket = APR_BRIGADE_FIRST(brigade);
                bucket != APR_BRIGADE_SENTINEL(brigade);
                bucket = APR_BUCKET_NEXT(bucket)) {

            const char * data;
            apr_size_t len;

            if (APR_BUCKET_IS_EOS(bucket)) {
                seen_eos = 1;
                break;
            }
            if (APR_BUCKET_IS_METADATA(bucket))
                continue;
            status = apr_bucket_read(bucket, &data, &len, APR_BLOCK_READ);
            if (status != APR_SUCCESS) {
                ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, "Can't bucket_read");
                ret = HTTP_INTERNAL_SERVER_ERROR;
                goto busted;
            }

            total_read += len;
            if (max && (total_read > max)) {
                ret = HTTP_REQUEST_ENTITY_TOO_LARGE;
                goto busted;
            }

            status = apr_file_write_full(fp, (void *) data, len, &len);
            if (status != APR_SUCCESS) {
                ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                             "Can't write to file '%s'", filename);
                ret = HTTP_INTERNAL_SERVER_ERROR;
                goto busted;
            }
        }
        apr_brigade_cleanup(brigade);
    } while (!seen_eos);

busted:
    apr_brigade_destroy(brigade);
    apr_file_close(fp);
    *size = total_read;
    return ret;
}

/* blast the request body straight into a file
 */
static int simple_put(request_rec * r, pub_t * pub) {
    char * dir;
    char * problem;
    char * tempname;
    apr_status_t status;
    int size;

    dir = dir_of(r->pool, r->filename);
    if ((problem = atom_dir(dir, r->pool))) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't create directory for PUT to '%s': %s",
                     r->uri, problem);
        return HTTP_FORBIDDEN;
    }
    tempname = temp_filename(r->pool, r->filename);
    status = store_incoming_bits(r, tempname, &size);
    if (status != OK)
        return server_error(r, status,
                            "Can't save to tempfile '%s' for PUT to '%s'",
                            tempname, r->uri);

    status = apr_file_rename(tempname, r->filename, r->pool);
    if (status != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Can't rename '%s' to '%s' for PUT to '%s'",
                     tempname, r->filename, r->uri);

        /* probably it was an illegal filename */
        return HTTP_FORBIDDEN;
    }
    return OK;
}

/* replace an existing media object */
static int replace_media(request_rec * r, entry_t * mle) {
    char * tempname;
    char * media_type = (char *) apr_table_get(r->headers_in, "Content-Type");
    char * filename;
    int saved;
    apr_status_t status;

    tempname = temp_filename(r->pool, r->filename);
    if ((saved = store_incoming_bits(r, tempname, &(mle->size))) != OK)
        return saved;

    status = apr_file_rename(tempname, r->filename, r->pool);
    if (status != APR_SUCCESS)
        return server_error(r, status,
                            "Can't rename '%s' to '%s", tempname, filename);

    /* now save the media type */
    filename = apr_psprintf(r->pool, "%s.%s", filename, MEDIA_TYPE_SUFFIX);
    to_file(filename, media_type, r->pool);

    return OK;
}

/* save an incoming media object, updating the media struct */
static int save_new_media(request_rec * r, entry_t * mle) {
    char * dir;
    char * yyyymmdd = yyyymmdd_at(r->request_time, r->pool);
    char * problem;
    char * filename;
    char * media_type = (char *) apr_table_get(r->headers_in, "Content-Type");
    char * tempname;
    pub_t * pub = mle->pub;
    int saved;
    apr_status_t status;

    /* URI for bits:
     *    collection uri minus /collection.atom + /yyyy/mm/dd/ + filename
     *  URI for mle: bits URI + ".atom"
     *  URI for media-type file: bits URI + ".mt"
     */
    dir = dir_of(r->pool, r->filename);
    dir = atom_join(r->pool, dir, yyyymmdd, NULL);

    if ((problem = atom_dir(dir, r->pool)))
        return server_error(r, APR_SUCCESS,
                            "save_new_media Can't access directory '%s': %s",
                            dir, problem);

    if ((filename = new_resource_filename(r, dir, mle)) == NULL)
        return HTTP_INTERNAL_SERVER_ERROR;

    /* for the mle->path, we now want to lose the pub->dir and
     *  ATOM_DIR off the front
     */
    mle->path = is_prefix(dir, pub->dir) + 1;
    mle->path += strlen(PUB_DIR);
    mle->path = atom_join(r->pool, mle->path, filename, NULL);

    filename = atom_join(r->pool, dir, filename, NULL);
    tempname = temp_filename(r->pool, filename);
    if ((saved = store_incoming_bits(r, tempname, &(mle->size))) != OK)
        return saved;

    if ((status = apr_file_rename(tempname, filename, r->pool)) != APR_SUCCESS)
        return server_error(r, status, "Can't rename '%s' to '%s",
                            tempname, filename);

    /* now save the media type */
    to_file(apr_psprintf(r->pool, "%s.%s", filename, MEDIA_TYPE_SUFFIX),
            media_type, r->pool);
    return OK;
}

/* save an entry into a file, updating fields in the entry struct
 *  This only works when called from a POST operation; it assumes r->filename
 *  points to the collection file
 */
static int save_new_entry(request_rec * r, entry_t * e) {
    apr_status_t status;
    char * dir;
    char * yyyymmdd = yyyymmdd_at(r->request_time, r->pool);
    char * problem;
    char * tempname;
    char * filename;
    char * sub_uri = NULL;

    /* First, find (make if necessary) the right directory.
     */
    dir = dir_of(r->pool, r->filename);
    dir = atom_join(r->pool, dir, yyyymmdd, NULL);

    /* dir now has the right directory, make sure it's there */
    if ((problem = atom_dir(dir, r->pool)))
        return server_error(r, APR_SUCCESS,
                            "save_new_entry Can't access directory '%s': %s",
                            dir, problem);

    /* generate a new filename in that directory */
    if ((filename = new_resource_filename(r, dir, e)) == NULL)
        return HTTP_INTERNAL_SERVER_ERROR;

    /* fill in the entry_t basics */
    if ((status = init_entry(r, dir, filename, e)) != OK)
        return status;

    /* if this is a meta, create the sub-pub now.  This has to be done
     *  here because we want to write a link to the sub-pub into the
     *  atom entry in the meta-pub, so we need to know its URI
     */
    if (e->pub->type == META_PUB) {
        if ((problem = create_sub_pub(r, e, &sub_uri)))
            return server_error(r, APR_SUCCESS,
                                "Can't create new publication in metapub %s: %s",
                                get_title(e->pub, r->pool), problem);
    }

    /* one time only per entry */
    e->id = new_id(r->pool);

    /* clean up entry */
    if ((problem = prepare_entry(r, e, sub_uri)) != NULL) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
                     "Problem preparing posted entry: %s", problem);
        return HTTP_FORBIDDEN;
    }

    /* blast it into a tempfile */
    tempname = apr_psprintf(r->pool, "%s/%s%ld-%ld",
                            dir, TEMP_PREFIX, random(), random());
    if ((problem = write_entry(r, e->xml, tempname)))
        return server_error(r, APR_SUCCESS,
                            "Can't write posted entry into '%s': %s",
                            tempname, problem);

    filename = entry_filename(r->pool, e);
    status = apr_file_rename(tempname, filename, r->pool);
    if (status != APR_SUCCESS)
        return server_error(r, status,
                            "Can't rename from '%s' to '%s' for POST",
                            tempname, filename);

    return OK;
}

/* it's in %-encoded UTF8.
 *  we'll turn it into UTF-8, then we'll turn anything that isn't an
 *  XML name character, or is a dot, into '-'
 */
static char * decode_slug(char * slug, apr_pool_t * pool) {
    utf8 decoded = (utf8) apr_pstrdup(pool, slug);
    utf8 cleaned;
    int in = 0, out = 0;
    int character;
    constUtf8 in_u;
    utf8 out_u;
    int dashed;

    while (slug[in]) {
        if (slug[in] != '%') {
            decoded[out++] = slug[in++];
            continue;
        }

        /* looking at %XX */
        in++;
        if (!(apr_isxdigit(slug[in]) && apr_isxdigit(slug[in + 1])))
            return NULL;
        sscanf(slug + in, "%02x", &character);

        /* per RFC5023 9.7.1, the UTF-8-izing has already been done, the %XX
         *  triples encode bytes of UTF-encoding, not UCS code points.
         */
        decoded[out++] = character;

        in += 2;
    }
    decoded[out] = 0;

    /* decoded now contains a pure UTF-8 version of the string */
    cleaned = (utf8) apr_pstrdup(pool, (char *) decoded);

    /* squish out illegal UTF-8 & non-XML chars.  Sam Ruby would say this
     *  is silent data loss, but that's OK, we give no guarantees aside from
     *  best-effort to create a reasonable URI
     */
    genxScrubText(char_util, (constUtf8) decoded, cleaned);
    if (*cleaned == 0)
        return NULL;

    /* now we'll walk through and move characters over, replacing everything
     *  that's not a NameChar with '-'
     */
    decoded = cleaned;
    cleaned = (utf8) apr_pstrdup(pool, (char *) decoded);
    in_u = decoded;
    out_u = cleaned;
    dashed = 1; /* suppress leading dashes */
    while (*in_u) {
        utf8 old_in = (utf8) in_u;
        int next_c = genxNextUnicodeChar(&in_u);
        if ((next_c != '.') &&
                (genxCharClass(char_util, next_c) & GENX_NAMECHAR)) {
            while (old_in < in_u)
                *out_u++ = *old_in++;
            dashed = 0;
        } else {
            /* suppress multiple dashes */
            if (!dashed)
                *out_u++ = '-';
            dashed = 1;
        }
    }
    *out_u = 0;

    /* now we have to %-encode again */
    decoded = cleaned;
    for (in = out = 0; decoded[in]; in++)
        if (decoded[in] < 128)
            out++;
        else
            out += 3;
    cleaned = (utf8) apr_palloc(pool, out + 1);
    for (in = out = 0; decoded[in]; in++) {
        if (decoded[in] == '%') {
            cleaned[out++] = '%';
            cleaned[out++] = '%';
        }
        if (decoded[in] < 128)
            cleaned[out++] = decoded[in];
        else {
            char s[4];
            sprintf(s, "%%%02X", decoded[in]);
            cleaned[out++] = s[0];
            cleaned[out++] = s[1];
            cleaned[out++] = s[2];
        }
    }
    cleaned[out] = 0;

    return (char *) cleaned;
}

/* Create the APP service doc; return NULL or an error message */
static char * write_service_doc(pub_t * pub, apr_pool_t * pool) {
    apr_file_t * service;
    char * problem;
    apr_status_t status;
    void * writer;
    char * service_file;
    char * val;

    service_file = apr_psprintf(pool, "%s/%s/%s",
                                pub->dir, PUB_DIR, SERVICE_FILE);

    /* we can do the following because we're only called at config time */
    /* XXX oops, no longer true; double-check */
    apr_file_remove(service_file, pool);
    status = apr_file_open(&service, service_file,
                           APR_FOPEN_WRITE | APR_FOPEN_CREATE, PERMS, pool);
    if (status != APR_SUCCESS)
        return apr_psprintf(pool, "Failed to open '%s', status: %d",
                            service_file, status);

    writer = atom_genx_file_writer(pool, service, &problem);
    if (!writer)
        return apr_psprintf(pool, "Can't start XML writer: '%s'", problem);

    atom_genx_start_elem(writer, APP_SERVICE_E);
    atom_genx_add_ns(writer, APP_NS_NUM);
    atom_genx_add_ns(writer, ATOM_NS_NUM);
    atom_genx_add_ns(writer, MOD_ATOM_NS_NUM);

    atom_genx_add_char(writer, '\n');
    atom_genx_start_elem(writer, APP_WORKSPACE_E);
    atom_genx_simple_elem(writer, ATOM_TITLE_E, get_title(pub, pool));

    atom_genx_start_elem(writer, APP_COLLECTION_E);
    if (pub->type == META_PUB)
        atom_genx_add_attr(writer, META_A, "service");
    val = apr_psprintf(pool, "%s/%s/%s/%s/%s", pub->path, ATOM_DIR,
                       ENTRIES_DIR, ENTRIES_COLL, COLLECTION_FILE);
    atom_genx_add_attr(writer, HREF_A, val);
    atom_genx_add_char(writer, '\n');
    atom_genx_simple_elem(writer, ATOM_TITLE_E, "Entries");
    atom_genx_end_elem(writer); /* collection */

    /* no media for metapubs */
    if (pub->type != META_PUB) {
        atom_genx_start_elem(writer, APP_COLLECTION_E);
        val = apr_psprintf(pool, "%s/%s/%s/%s/%s", pub->path, ATOM_DIR,
                           MEDIA_DIR, MEDIA_COLL, COLLECTION_FILE);
        atom_genx_add_attr(writer, HREF_A, val);
        atom_genx_add_char(writer, '\n');
        atom_genx_simple_elem(writer, ATOM_TITLE_E, "Media");
        atom_genx_simple_elem(writer, APP_ACCEPT_E, "*/*");
        atom_genx_end_elem(writer); /* collection */
        atom_genx_add_char(writer, '\n');
    }

    atom_genx_end_elem(writer); /* workspace */
    atom_genx_end_elem(writer); /* service */
    atom_genx_end_doc(writer);

    apr_file_close(service);
    return NULL;
}

/* got tired of calling apr_stat */
int atom_file_exists(char * name, apr_pool_t * pool) {
    apr_finfo_t finfo;
    return (apr_stat(&finfo, name, APR_FINFO_MIN, pool) == APR_SUCCESS);
}

/* If the named file doesn't exist, initialize it from the provided data
 */
static char * init_file(apr_pool_t * pool, char * fname, char * contents) {
    char * problem;
    if (!atom_file_exists(fname, pool)) {
        if ((problem = to_file(fname, contents, pool)))
            return problem;
    }
    return NULL;
}

/* probe to see if a pub exists; create it if not.  Return an error message
 *  if unsuccessful.
 */
static char * build_pub(pub_t * pub, apr_pool_t * pperm, apr_pool_t * ptemp) {
    char * s;
    static char * yyyymmdd = "[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]";
    char * problem;
    char * dirname;
    apr_dir_t * dir;
    apr_finfo_t finfo;

    /* We need to ensure that the subdirectory tree exists & is writeable,
     *  and that the ENTRIES and MEDIA collections exist.
     */
    if ((problem = atom_dir(pub->dir, ptemp)))
        return problem;

    /* Make the extras directory tree & subtrees */
    s = atom_join(ptemp, pub->dir, PUB_DIR, EXTRAS_DIR, NULL);
    if ((problem = atom_dir(s, ptemp)))
        return problem;
    if ((problem = atom_dir(atom_join(ptemp, s, TEMPLATES_DIR, NULL), ptemp)))
        return problem;
    if ((problem = atom_dir(atom_join(ptemp, s, JS_DIR, NULL), ptemp)))
        return problem;
    if ((problem = atom_dir(atom_join(ptemp, s, CSS_DIR, NULL), ptemp)))
        return problem;
    if ((problem = atom_dir(atom_join(ptemp, s, OPTIONS_DIR, NULL), ptemp)))
	return problem;

    /* make the /meta/ s00 .. s99 subdirs for metapubs */
    if (pub->type == META_PUB) {
        int i;
        for (i = 0; i < 100; i++) {
            char * sname = apr_psprintf(ptemp, "%s%02d", METAPUB_SUBS, i);
            if ((problem = atom_dir(atom_join(ptemp, pub->dir, sname, NULL),
                                    ptemp)))
                return problem;
        }
    }

    /* lots of this stuff doesn't get built for metapubs.  Could have one
     *  big conditional but I like the flow this way.
     */
    if (pub->type != META_PUB)
        if ((problem = atom_dir(atom_join(ptemp, pub->dir, PUB_DIR, NULL),
                                ptemp)))
            return problem;

    s = atom_join(ptemp, pub->dir, PUB_DIR, ENTRIES_DIR, ENTRIES_COLL, NULL);
    if ((problem = atom_dir(s, ptemp)))
        return problem;
    if (pub->type != META_PUB) {
        s = atom_join(ptemp, pub->dir, PUB_DIR, MEDIA_DIR, MEDIA_COLL, NULL);
        if ((problem = atom_dir(s, ptemp)))
            return problem;
    }

    /* if there's no unique ID for the public-facing feed, write one now */
    if (pub->type != META_PUB) {
        s = atom_join(ptemp, pub->dir, PUB_DIR, ID_FILE, NULL);
        if ((problem = init_file(ptemp, s, new_id(ptemp))))
            return apr_psprintf(ptemp, "Can't create ID file '%s': %s",
                                s, problem);
    }

    if (pub->type != META_PUB) {
        /* write the initial entry CSS & JS files */
        char * base = atom_join(ptemp, pub->dir, PUB_DIR, EXTRAS_DIR, NULL);
        s = atom_join(ptemp, base, CSS_DIR, ENTRY_CSS, NULL);
        if ((problem = init_file(ptemp, s, default_atom_entry_css)))
            return problem;

        s = atom_join(ptemp, base, JS_DIR, ENTRY_JS, NULL);
        if ((problem = init_file(ptemp, s, default_atom_entry_js)))
            return problem;

        /* write the initial front-page CSS & JS files */
        s = atom_join(ptemp, base, CSS_DIR, FRONT_PAGE_CSS, NULL);
        if ((problem = init_file(ptemp, s, default_front_page_css)))
            return problem;

        s = atom_join(ptemp, base, JS_DIR, FRONT_PAGE_JS, NULL);
        if ((problem = init_file(ptemp, s, default_front_page_js)))
            return problem;

        /* write the initial front & entry HTML templates */
        dirname = atom_join(ptemp, pub->dir, PUB_DIR, EXTRAS_DIR,
                            TEMPLATES_DIR, NULL);
        s = atom_join(ptemp, dirname, ENTRY_TEMPLATE, NULL);
        if ((problem = init_file(ptemp, s, default_entry_page_template)))
            return problem;

        s = atom_join(ptemp, dirname, FRONT_PAGE_TEMPLATE, NULL);
        if ((problem = init_file(ptemp, s, default_front_page_template)))
            return problem;

	/* by default, HTML is written */
	s = atom_join(ptemp, pub->dir, PUB_DIR, EXTRAS_DIR, OPTIONS_DIR,
		      HTML_OPTIONS_FILE, NULL);
	if ((problem = init_file(ptemp, s, "")))
	    return problem;
    }

    /* pre-compute a bunch of strings for query-time uri analysis;
     *  note these run against the URI not the directory.
     */
    if (pub->type != SUB_PUB) {
        char * sub_prefix;
        char * pubatom;

        sub_prefix = apr_psprintf(ptemp, "%s(/%s[0-9][0-9]/[^/]*)?/",
                                  pub->path, METAPUB_SUBS);
        pubatom = "(pub|atom)/";
        pub->patterns = (pub_patterns_t *)
                apr_palloc(pperm, sizeof (pub_patterns_t));

        /* = a regex to detect the extras space */
        s = atom_join(ptemp, sub_prefix, pubatom, EXTRAS_DIR, NULL);
        pub->patterns->extras_space = ap_pregcomp(pperm, s, AP_REG_EXTENDED);

        /* - a regex to detect atom entries */
        s = atom_join(ptemp, sub_prefix, pubatom, NULL);
        s = apr_psprintf(ptemp, "^%s/[^/]*/[^/]*/%s/[^.][^.]*\\.atom$", s,
                         yyyymmdd);
        pub->patterns->entry = ap_pregcomp(pperm, s, AP_REG_EXTENDED);

        /* - a regex to match a collection */
        s = atom_join(ptemp, sub_prefix, pubatom, NULL);
        s = apr_psprintf(ptemp, "^%s/[^/]*/[^/]*/%s", s, COLLECTION_FILE);
        pub->patterns->collection = ap_pregcomp(pperm, s, AP_REG_EXTENDED);

        /* - a regex to match a media object */
        s = atom_join(ptemp, sub_prefix, pubatom, MEDIA_DIR, NULL);
        s = apr_psprintf(ptemp, "^%s/[^/]*/%s/[^.][^.]*$", s, yyyymmdd);
        pub->patterns->media_object = ap_pregcomp(pperm, s, AP_REG_EXTENDED);

        /* - a prefix to match entry/media collections */
        s = atom_join(ptemp, sub_prefix, pubatom, ENTRIES_DIR, NULL);
        pub->patterns->entries_space = ap_pregcomp(pperm, s, AP_REG_EXTENDED);
        s = atom_join(ptemp, sub_prefix, pubatom, MEDIA_DIR, NULL);
        pub->patterns->media_space = ap_pregcomp(pperm, s, AP_REG_EXTENDED);

	/* and the pre-cooked name of its HTML options file */
	pub->options_html_file =
	    atom_join(pperm, pub->dir, PUB_DIR, EXTRAS_DIR,
		      OPTIONS_DIR, HTML_OPTIONS_FILE, NULL);
    }

    /* Recreate: service doc, collection docs, index.html
     */
    if ((problem = write_service_doc(pub, ptemp)))
        return problem;

    /* any subdirectory of MEDIA_DIR or ENTRIES_DIR is a collection,
     *  so it needs a feed written
     */
    dirname = apr_psprintf(ptemp, "%s/%s/%s", pub->dir, PUB_DIR, ENTRIES_DIR);

    /* this routine has already successfully visited this dir,
     *  thus the lack of checking
     */
    apr_dir_open(&dir, dirname, ptemp);
    while (apr_dir_read(&finfo, APR_FINFO_NAME | APR_FINFO_TYPE, dir) == APR_SUCCESS) {
        if (finfo.filetype == APR_DIR && index(finfo.name, '.') == NULL) {
            char * coll = atom_join(ptemp, dirname, finfo.name, NULL);
            char * id_name = atom_join(ptemp, coll, ID_FILE, NULL);

            /* if there's no ID file (collection being created) make one */
            if ((problem = init_file(ptemp, id_name, new_id(ptemp)))) {
                apr_dir_close(dir);
                return problem;
            }

            if ((problem = write_collection(ptemp, pub, coll,
                                            DEFAULT_ENTRIES_IN_FEED))) {
                apr_dir_close(dir);
                return problem;
            }
        }
    }
    apr_dir_close(dir);

    if (pub->type != META_PUB) {
        dirname = atom_join(ptemp, pub->dir, PUB_DIR, MEDIA_DIR, NULL);

        /* this routine has already successfully visited this dir, thus the
         * lack of checking */
        apr_dir_open(&dir, dirname, ptemp);
        while (apr_dir_read(&finfo, APR_FINFO_NAME | APR_FINFO_TYPE, dir) == APR_SUCCESS) {
            if (finfo.filetype == APR_DIR && index(finfo.name, '.') == NULL) {
                char * coll = atom_join(ptemp, dirname, finfo.name, NULL);
                char * id_name = atom_join(ptemp, coll, ID_FILE, NULL);

                /* if there's no ID file (collection being created) make one */
                if ((problem = init_file(ptemp, id_name, new_id(ptemp)))) {
                    apr_dir_close(dir);
                    return problem;
                }

                if ((problem = write_collection(ptemp, pub, coll,
                                                DEFAULT_ENTRIES_IN_FEED))) {

                    apr_dir_close(dir);
                    return problem;
                }
            }
        }
        apr_dir_close(dir);
    }

    /* XXX Now we have to update_public_site after creating an ID file
     *  in pub/e/entries/
     */
    return NULL;
}

/* does name represent a writeable directory, which we may need to
 *  create as a side effect?
 */
static char * atom_dir(char * name, apr_pool_t * p) {
    apr_finfo_t finfo;
    char * tempname;
    apr_file_t * f;
    apr_status_t s = apr_stat(&finfo, name, APR_FINFO_TYPE, p);

    if (s == APR_SUCCESS) {
        if (finfo.filetype != APR_DIR)
            return apr_psprintf(p, "'%s' is not a directory", name);
    } else {
        /* couldn't stat so let's try to make it */
        s = apr_dir_make_recursive(name, PERMS, p);
        if (s != APR_SUCCESS)
            return apr_psprintf(p, "Can't create directory '%s'", name);
    }

    /* we want to see if the directory is writeable... we could screw
     *  around with the stupid uid_current and all that, or we could just
     *  try to open a file.
     */
    tempname = apr_psprintf(p, "%s/%ld-%ld.tmp", name, random(), random());
    s = apr_file_open(&f, tempname,
                      APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_DELONCLOSE,
                      PERMS, p);
    if (s == APR_SUCCESS) {
        apr_file_close(f);
        return NULL;
    }
    else
        return apr_psprintf(p, "Directory '%s' is not writeable", name);

}

/* Build index structure for looking up if a uri is in the URI space
 *  of one of our pubs.
 *
 * Here's the problem; we have to look at every URI to decide if
 *  if it falls into the URI space of one of our pubs.  This is easy
 *  because it's prefix-matching.  Unfortunately, if we have a lot of pubs,
 *  it'll be an unacceptable tax on the whole httpd.
 * One plan would be to have a big regex with
 *  ^(pub1|pub2|pub3|... )
 *  which would tell us yes-no but then we'd still have to run through
 *  all the pubs, and if this server is *mostly* going to be servicing
 *  our pubs, we'll end up with sequences of prefix checks.  You could get
 *  clever and do:
 *  ^((pub1)|(pub2)|... )
 *  then look for $2 and $3 and so on, but unfortunately AP_MAX_REG_MATCH
 *  is 10.
 *  So what we do, since we know that URI characters are only in the range
 *  0-127, is make a simple DFA with arrays of 128 ints.  This makes
 *  the check for being in the URI space a pretty-fast linear function of
 *  the unique prefix.  We use ints rather than chars because when you
 *  actually match, you need to signal this isn't another turn of the DFA
 *  crank, it's a match, and the rest of the bits give you an index into "pubs".
 * Each state in the DFA is an int[] indexed by the current character;
 *  entry value of 0 means fail-to-match, a positive integer is the index
 *  of the next state, a negative integer indicates a successful match.
 */

/* set up the index-building recursion */
static int build_index(apr_pool_t * pperm, apr_pool_t * ptemp) {
    int * state0;
    int * * slot0;
    int * mask0 = (int *) apr_palloc(ptemp, pubs_from_config->nelts * sizeof (int));
    int i;
    int ret;

    pub_index = apr_array_make(pperm, 10, sizeof (int *));
    state0 = (int *) apr_pcalloc(pperm, 128 * sizeof (int));
    slot0 = (int * *) apr_array_push(pub_index);
    *slot0 = state0;

    for (i = 0; i < pubs_from_config->nelts; i++)
        mask0[i] = 1;
    ret = index_step(state0, mask0, 0, pperm, ptemp);

    /* XXX log the size of the DFA */
    return ret;
}

/* build one level of the pub-finding DFA (see big comments above).
 * The idea is that all the pubs in the subset of pubs_from_config flagged in
 *  this_mask have a common prefix of length pref_length, so we're going
 *  to look at the next letter in each & figure out where the common-prefix
 *  runs out.
 * Return 0 if all went OK, 1 if error.
 */
static int index_step(int * this_state, int * this_mask, int prefix_length,
                      apr_pool_t * pperm, apr_pool_t * ptemp) {
    int * letter_counts = (int *) apr_pcalloc(ptemp, 128 * sizeof (int));
    pub_t * publist = (pub_t *) pubs_from_config->elts;
    int pub;
    int letter;

    for (pub = 0; pub < pubs_from_config->nelts; pub++)
        if (this_mask[pub])
            letter_counts[(int) (publist[pub].path[prefix_length])]++;

    /* more than one pub with the same base uri is a bug */
    if (letter_counts[0] > 1) {
        /* XXX TODO log an error */
        return 1;
    }

    for (letter = 0; letter < 128; letter++) {
        if (letter_counts[letter] == 0)
            continue;

        else if (letter_counts[letter] == 1) {
            for (pub = 0; pub < pubs_from_config->nelts; pub++) {
                if (this_mask[pub] && (publist[pub].path[prefix_length] == letter)) {
                    this_state[letter] = -1 * (pub + 1); /* -1 * 0 == 0, sigh */
                    break;
                }
            }
        } else {
            /* need to build another state in the machine */
            int * next_mask =
                    (int *) apr_pcalloc(ptemp, pubs_from_config->nelts * sizeof (int));
            int next_index = pub_index->nelts;
            int * * next_slot = (int * *) apr_array_push(pub_index);
            int * next_state = (int *) apr_pcalloc(pperm, 128 * sizeof (int));
            *next_slot = next_state;

            for (pub = 0; pub < pubs_from_config->nelts; pub++) {
                if (this_mask[pub] && (publist[pub].path[prefix_length] == letter)) {
                    next_mask[pub] = 1;
                    this_state[letter] = next_index;
                }
            }

            if (index_step(next_state, next_mask, prefix_length + 1, pperm, ptemp))

                return 1;
        }
    }
    return 0;
}

/* There's a request_uri which falls in the URI space of a meta-pub.  The pub
 *  argument is the pub_t for the meta-pub, which may what the request wants;
 *  alternatively, the access may be to one of the sub-pubs.
 */
static pub_t * find_metapub(pub_t * pub, request_rec * r, char * suffix) {
    pub_t * sub;
    char * coll_fn;
    char * slash;

    /* if the path doesn't start with METAPUB_SUBS, it's the meta-pub */
    if (suffix[0] == '/')
        suffix++;

    if (MATCHER(sub_path_el, suffix) != 0)
        return pub;

    /* don't want to screw with the incoming URI */
    suffix = apr_pstrdup(r->pool, suffix);

    /* so the suffix now starts with /s\d\d/pub-name/.... */
    slash = index(suffix, '/');
    if (slash == NULL)
        return NULL;

    slash = index(slash + 1, '/');
    if (slash == NULL)
        return NULL;

    *slash = 0;

    /* now we'll see if the sub actually exists */
    coll_fn = atom_join(r->pool, pub->dir, suffix, PUB_DIR, ENTRIES_DIR,
                        ENTRIES_COLL, COLLECTION_FILE, NULL);

    if (!atom_file_exists(coll_fn, r->pool))
        return NULL;

    /* OK, the pub exists.  We'll fill in the parts we can */
    sub = (pub_t *) apr_palloc(r->pool, sizeof (pub_t));
    sub->path = atom_join(r->pool, pub->path, suffix, NULL);
    sub->dir = atom_join(r->pool, pub->dir, suffix, NULL);
    sub->title = sub->author = NULL;
    sub->patterns = pub->patterns;
    sub->options_html_file = atom_join(r->pool, pub->dir, PUB_DIR, EXTRAS_DIR,
				       OPTIONS_DIR, HTML_OPTIONS_FILE, NULL);

    sub->type = SUB_PUB;

    return sub;
}

/* Load up the fields in a (necessarily sub-) pub that require reading
 *  the collection; currently only author and title.  No error-checking
 *  since I don't see how this can fail absent some catastrophic resource
 *  shortage that's going to end the world.
 */
static void load_pub(pub_t * pub, apr_pool_t * pool) {
    char * coll_fn = atom_join(pool, pub->dir, PUB_DIR, ENTRIES_DIR,
                               ENTRIES_COLL, COLLECTION_FILE, NULL);
    apr_xml_doc * coll;
    apr_xml_elem * title, * author;
    char * wanted[] = { "title", "author", NULL };
    apr_xml_elem * * children[] = { &title, &author };

    xnarf(coll_fn, &coll, pool);
    atom_xml_children(coll, coll->root, ATOM_NS, wanted, children);
    if (title)
	pub->title = element_text(title, pool);
    if (author) {
	apr_xml_elem * name; 
	if ((name = atom_xml_child(coll, coll->root, ATOM_NS, "name")))
	    pub->author = element_text(name, pool);
    }
}

/* getters for fields that may require loading the pub from its
 *  collection file.
 */
static char * get_author(pub_t * pub, apr_pool_t * pool) {

    if (!pub->author)
        load_pub(pub, pool);

    return pub->author;
}

static char * get_title(pub_t * pub, apr_pool_t * pool) {

    if (!pub->title)
        load_pub(pub, pool);

    return pub->title;
}

/* is this URI one of ours? If so, return its pub_t structure, else NULL */

/* Hmm, this will blow chunks if someone manages to slip in a uri with
 *  non-ASCII characters.  They should be %-encoded by the time they get
 *  here, right?
 */
static pub_t * find_pub(request_rec * r) {
    pub_t * publist = (pub_t *) pubs_from_config->elts;
    int * * states = (int * *) pub_index->elts;
    int * state = states[0];
    char * uri = r->uri;

    pub_t * pub;
    if ((pub = (pub_t *) apr_table_get(r->notes, uri)))
        return pub;

    while (*uri) {
        int next = state[(int) (*uri++)];
        if (next == 0)
            return NULL;
        else if (next < 0) {
            char * suffix;
            pub = publist + ((next * -1) - 1);
            if ((suffix = is_prefix(r->uri, pub->path))) {
                if (pub->type == META_PUB)
                    pub = find_metapub(pub, r, suffix);

                apr_table_setn(r->notes, r->uri, (char *) pub);
                return pub;
            } else
                return NULL;
        }
        else
            state = states[next];
    }

    /* prefix was ambiguous, we ran out of uri with multiple possibilities
     *  still in play
     */
    /* XXX TODO log an error message */
    return NULL;
}

/* Discover whether 'prefix' is a prefix of s. Return NULL if not; if it is,
 *  return a pointer to the part of s following the prefix
 */
static char * is_prefix(char * s, char * prefix) {

    for (; *prefix && *s && (*prefix == *s); s++, prefix++)
        /* empty */;

    return (*prefix == 0) ? s : NULL;
}

static char * yyyymmdd_at(apr_time_t when, apr_pool_t * pool) {

    apr_time_exp_t times;

    apr_time_exp_lt(&times, when);

    return apr_psprintf(pool, "%04d/%02d/%02d", times.tm_year + 1900,
                        times.tm_mon + 1, times.tm_mday);
}

/* The problem is that mod_atom is forever gluing together fragments to make
 *  pathnames of one kind or another.  It's tough to keep track of whether
 *  or not the fragments end with '/', particularly given that the AtomPub
 *  directive arguments might or might not have slashes on the end.
 * So this glues its arguments together, ensuring that there is exactly
 *  one connecting slash.
 */
char * atom_join(apr_pool_t * pool, ...) {
    va_list va;
    char * path;
    char * joined;
    int buf_len = 0;
    int in, out;

    /* Figure out how much memory we'll need.  You could avoid this step, but
     *  it'd cost you a function call per character.  Alternatively, you could
     *  build the answer in a local char[24000] or something which would *never*
     *  fail but That Would Be Wrong.
     */
    va_start(va, pool);
    while ((path = va_arg(va, char *)))
        buf_len += strlen(path) + 1;
    joined = apr_palloc(pool, buf_len + 1);

    out = 0;
    va_start(va, pool);
    while ((path = va_arg(va, char *))) {
        in = (out != 0 && path[0] == '/') ? 1 : 0;
        while (path[in])
            joined[out++] = path[in++];

        if (in > 0 && path[in - 1] != '/')
            joined[out++] = '/';
    }

    if (out > 0 && joined[out - 1] == '/')
        out--;
    joined[out] = 0;

    return joined;
}


/* register our interests */
static void hooks(apr_pool_t * p) {

    ap_hook_pre_config(pre_config, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_post_config(post_config, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_handler(handler, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_type_checker(type_checker, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_map_to_storage(map_to_storage, NULL, NULL, APR_HOOK_MIDDLE);
}

static const command_rec directives[] = {
    AP_INIT_TAKE_ARGV("AtomPub", atompub_directive, NULL, RSRC_CONF,
                      "AtomPub uri-path real-dir title author"),
    AP_INIT_TAKE_ARGV("AtomMetaPub", atommetapub_directive, NULL, RSRC_CONF,
                      "AtomMetaPub uri-path real-dir title author"), {
        NULL}
};

module AP_MODULE_DECLARE_DATA atom_module = {
    STANDARD20_MODULE_STUFF,
    NULL, /* create per-directory config structure */
    NULL, /* merge per-directory config structures */
    NULL, /* create per-server config structure */
    NULL, /* merge per-server config structures */
    directives, /* handle config directive */
    hooks /* registrar */
};

